Completed
Push — develop ( f91ec9...875570 )
by A
49s
created

PortRange.__str__()   A

Complexity

Conditions 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
c 2
b 0
f 0
dl 0
loc 8
rs 9.4285
1
# -*- coding: utf-8 -*-
2
#
3
# Copyright (c) 2014-2015 Scaleway and Contributors. All Rights Reserved.
4
#                         Kevin Deldycke <[email protected]>
5
#                         Gilles Dartiguelongue <[email protected]>
6
#
7
# Licensed under the BSD 2-Clause License (the "License"); you may not use this
8
# file except in compliance with the License. You may obtain a copy of the
9
# License at http://opensource.org/licenses/BSD-2-Clause
10
11
""" Port range utilities and helpers.
12
"""
13
14
from __future__ import (absolute_import, division, print_function,
15
                        unicode_literals)
16
17
import math
18
19
try:
20
    basestring
21
except NameError:  # pragma: no cover
22
    basestring = (str, bytes)  # pylint: disable=C0103
23
24
try:
25
    from itertools import imap as iter_map
26
except ImportError:  # pragma: no cover
27
    iter_map = map
28
29
30
__version__ = '1.0.6'
31
32
33
class PortRange(object):
34
    """ Port range with support of a CIDR-like (binary) notation.
35
36
    In strict mode (disabled by default) we'll enforce the following rules:
37
        * port base must be a power of two (offsets not allowed);
38
        * port CIDR should not produce overflowing upper bound.
39
40
    This mode can be disabled on object creation.
41
    """
42
    # Separators constants
43
    CIDR_SEP = '/'
44
    RANGE_SEP = '-'
45
46
    # Max port lenght, in bits
47
    port_lenght = 16
48
    # Max port range integer values
49
    port_min = 1
50
    port_max = (2 ** port_lenght) - 1
51
52
    # Base values on which all other properties are computed.
53
    port_from = None
54
    port_to = None
55
56
    def __init__(self, port_range, strict=False):
57
        """ Set up class with a port_from and port_to integer. """
58
        self.port_from, self.port_to = self.parse(port_range, strict=strict)
59
60
    def parse(self, port_range, strict=False):
61
        """ Parse and normalize port range string into a port range. """
62
        if isinstance(port_range, basestring) and self.CIDR_SEP in port_range:
63
            base, prefix = self.parse_cidr(port_range, strict)
64
            port_from, port_to = self._cidr_to_range(base, prefix)
65
        else:
66
            port_from, port_to = self.parse_range(port_range)
67
        if not port_from or not port_to:
68
            raise ValueError("Invalid ports.")
69
        # Check upper bound
70
        if strict:
71
            # Disallow overflowing upper bound
72
            if port_to > self.port_max:
73
                raise ValueError("Overflowing upper bound.")
74
        else:
75
            # Cap upper bound
76
            port_to = port_to if port_to < self.port_max else self.port_max
77
        return port_from, port_to
78
79
    def parse_cidr(self, port_range, strict=False):
80
        """ Split a string and extract port base and CIDR prefix.
81
82
        Always returns a list of 2 integers. Defaults to None.
83
        """
84
        # Separate base and prefix
85
        elements = list(iter_map(int, port_range.split(self.CIDR_SEP, 2)))
86
        elements += [None, None]
87
        base, prefix = elements[:2]
88
        # Normalize prefix value
89
        if prefix is None:
90
            prefix = self.port_lenght
91
        # Validates base and prefix values
92
        if not base or base < self.port_min or base > self.port_max:
93
            raise ValueError("Invalid port base.")
94
        if not prefix or prefix < 1 or prefix > self.port_lenght:
95
            raise ValueError("Invalid CIDR-like prefix.")
96
        # Enable rigorous rules
97
        if strict:
98
            # Disallow offsets
99
            if prefix != self.port_lenght and not self._is_power_of_two(base):
100
                raise ValueError("Port base is not a power of Two.")
101
        return base, prefix
102
103
    def parse_range(self, port_range):
104
        """ Normalize port range to a sorted list of no more than 2 integers.
105
106
        Excludes None values while parsing.
107
        """
108
        if isinstance(port_range, basestring):
109
            port_range = port_range.split(self.RANGE_SEP, 2)
110
        if not isinstance(port_range, (set, list, tuple)):
111
            port_range = [port_range]
112
        port_range = [int(port)
113
                      for port in port_range
114
                      if port and int(port)][:2]
115
        port_range.sort()
116
        # Fill out missing slots by None values
117
        port_range += [None] * (2 - len(port_range))
118
        port_from, port_to = port_range
119
        if not port_from or port_from < self.port_min or \
120
                port_from > self.port_max:
121
            raise ValueError("Invalid port range lower bound.")
122
        if not port_to:
123
            port_to = port_from
124
        return port_from, port_to
125
126
    def __repr__(self):
127
        """ Print all components of the range. """
128
        return '{}(port_from={}, port_to={}, base={}, offset={}, prefix={}, ' \
129
            'mask={})'.format(self.__class__.__name__, self.port_from,
130
                              self.port_to, self.base, self.offset,
131
                              self.prefix, self.mask)
132
133
    def __str__(self):
134
        """ Returns the most appropriate string representation. """
135
        if self.is_single_port:
136
            return str(self.port_from)
137
        try:
138
            return self.cidr_string
139
        except ValueError:
140
            return self.range_string
141
142
    @property
143
    def cidr_string(self):
144
        """ Returns a clean CIDR-like notation if possible. """
145
        if not self.is_cidr:
146
            raise ValueError(
147
                "Range can't be rendered using a CIDR-like notation.")
148
        return '{}{}{}'.format(self.base, self.CIDR_SEP, self.prefix)
149
150
    @property
151
    def range_string(self):
152
        """ Returns a clean range notation. """
153
        return '{}{}{}'.format(self.port_from, self.RANGE_SEP, self.port_to)
154
155
    @classmethod
156
    def _is_power_of_two(cls, value):
157
        """ Helper to check if a value is a power of 2. """
158
        return math.log(value, 2) % 1 == 0
159
160
    @classmethod
161
    def _nearest_power_of_two(cls, value):
162
        """ Returns nearsest power of 2. """
163
        return int(2 ** math.floor(math.log(value, 2)))
164
165
    @classmethod
166
    def _mask(cls, prefix):
167
        """ Compute the mask. """
168
        return cls.port_lenght - prefix
169
170
    @classmethod
171
    def _raw_upper_bound(cls, base, prefix):
172
        """ Compute a raw upper bound. """
173
        return base + (2 ** cls._mask(prefix)) - 1
174
175
    @classmethod
176
    def _cidr_to_range(cls, base, prefix):
177
        """ Transform a CIDR-like notation into a port range. """
178
        port_from = base
179
        port_to = cls._raw_upper_bound(base, prefix)
180
        return port_from, port_to
181
182
    @property
183
    def bounds(self):
184
        """ Returns lower and upper bounds of the port range. """
185
        return self.port_from, self.port_to
186
187
    @property
188
    def base(self):
189
        """ Alias to port_from, used as a starting point for CIDR notation. """
190
        return self.port_from
191
192
    @property
193
    def offset(self):
194
        """ Port base offset from its nearest power of two. """
195
        return self.base - self._nearest_power_of_two(self.base)
196
197
    @property
198
    def prefix(self):
199
        """ A power-of-two delta means a valid CIDR-like prefix. """
200
        # Check that range delta is a power of 2
201
        port_delta = self.port_to - self.port_from + 1
202
        if not self._is_power_of_two(port_delta):
203
            return None
204
        return self.port_lenght - int(math.log(port_delta, 2))
205
206
    @property
207
    def mask(self):
208
        """ Port range binary mask, based on CIDR-like prefix. """
209
        return self._mask(self.prefix) if self.prefix else None
210
211
    @property
212
    def cidr(self):
213
        """ Returns components of the CIDR-like notation. """
214
        return self.base, self.prefix
215
216
    @property
217
    def is_single_port(self):
218
        """ Is the range a single port ? """
219
        return True if self.port_from == self.port_to else False
220
221
    @property
222
    def is_cidr(self):
223
        """ Is the range can be expressed using a CIDR-like notation. """
224
        return True if self.prefix is not None else False
225