PortRange.cidr_string()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
1
# -*- coding: utf-8 -*-
2
#
3
# Copyright (c) 2014-2016 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 https://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
from collections import Iterable
19
20
try:
21
    from itertools import imap as iter_map
22
except ImportError:  # pragma: no cover
23
    iter_map = map
24
25
try:
26
    basestring
27
except NameError:  # pragma: no cover
28
    basestring = (str, bytes)  # pylint: disable=C0103
29
30
__version__ = '2.1.1'
31
32
33
class PortRange(object):
34
35
    """ Port range with support of a CIDR-like (binary) notation.
36
37
    In strict mode (disabled by default) we'll enforce the following rules:
38
        * port base must be a power of two (offsets not allowed);
39
        * port range must be within the 1-65535 inclusive range.
40
41
    This mode can be disabled on object creation.
42
    """
43
44
    # Separators constants for CIDR and range notation.
45
    CIDR_SEP = '/'
46
    RANGE_SEP = '-'
47
48
    # Max port length, in bits.
49
    port_length = 16
50
    # Max port range integer values
51
    port_min = 1
52
    port_max = (2 ** port_length) - 1
53
54
    # Base values on which all other properties are computed.
55
    port_from = None
56
    port_to = None
57
58
    def __init__(self, port_range, strict=False):
59
        """ Set up class with a port_from and port_to integer. """
60
        self.strict = strict
61
        self.port_from, self.port_to = self.parse(port_range)
62
63
    def parse(self, port_range):
64
        """ Parse and normalize a string or iterable into a port range. """
65
        # Any string containing a CIDR separator is parsed as a CIDR-like
66
        # notation, others as a range or single port.
67
        cidr_notation = False
68
        if isinstance(port_range, basestring):
69
            cidr_notation = self.CIDR_SEP in port_range
70
            separator = self.CIDR_SEP if cidr_notation else self.RANGE_SEP
71
            port_range = port_range.split(separator, 1)
72
73
        # We expect here a list of elements castable to integers.
74
        if not isinstance(port_range, Iterable):
75
            port_range = [port_range]
76
        try:
77
            port_range = list(iter_map(int, port_range))
78
        except TypeError:
79
            raise ValueError("Can't parse range as a list of integers.")
80
81
        # At this point we should have a list of one or two integers.
82
        if not 0 < len(port_range) < 3:
83
            raise ValueError("Expecting a list of one or two elements.")
84
85
        # Transform CIDR notation into a port range and validates it.
86
        if cidr_notation:
87
            base, prefix = port_range
88
            port_range = self._cidr_to_range(base, prefix)
89
90
        # Let the parser fix a reverse-ordered range in non-strict mode.
91
        if not self.strict:
92
            port_range.sort()
93
94
        # Get port range bounds.
95
        port_from = port_range[0]
96
        # Single port gets their upper bound set to None.
97
        port_to = port_range[1] if len(port_range) == 2 else None
98
99
        # Validate constraints in strict mode.
100
        if self.strict:
101
            # Disallow out-of-bounds values.
102
            if not (self.port_min <= port_from <= self.port_max) or (
103
                    port_to is not None and not (
104
                        self.port_min <= port_to <= self.port_max)):
105
                raise ValueError("Out of bounds.")
106
            # Disallow reversed range.
107
            if port_to is not None and port_from > port_to:
108
                raise ValueError("Invalid reversed port range.")
109
110
        # Clamp down lower bound, then cap it.
111
        port_from = min([max([port_from, self.port_min]), self.port_max])
112
113
        # Single port gets its upper bound aligned to its lower one.
114
        if port_to is None:
115
            port_to = port_from
116
117
        # Cap upper bound.
118
        port_to = min([port_to, self.port_max])
119
120
        return port_from, port_to
121
122
    def __repr__(self):
123
        """ Print all components of the range. """
124
        return (
125
            '{}(port_from={}, port_to={}, base={}, offset={}, prefix={}, '
126
            'mask={}, is_single_port={}, is_cidr={})').format(
127
                self.__class__.__name__, self.port_from, self.port_to,
128
                self.base, self.offset, self.prefix, self.mask,
129
                self.is_single_port, self.is_cidr)
130
131
    def __str__(self):
132
        """ Return the most appropriate string representation. """
133
        if self.is_single_port:
134
            return str(self.port_from)
135
        try:
136
            return self.cidr_string
137
        except ValueError:
138
            return self.range_string
139
140
    @property
141
    def cidr_string(self):
142
        """ Return a clean CIDR-like notation if possible. """
143
        if not self.is_cidr:
144
            raise ValueError(
145
                "Range can't be rendered using a CIDR-like notation.")
146
        return '{}{}{}'.format(self.base, self.CIDR_SEP, self.prefix)
147
148
    @property
149
    def range_string(self):
150
        """ Return a clean range notation. """
151
        return '{}{}{}'.format(self.port_from, self.RANGE_SEP, self.port_to)
152
153
    @classmethod
154
    def _is_power_of_two(cls, value):
155
        """ Helper to check if a value is a power of 2. """
156
        return math.log(value, 2) % 1 == 0
157
158
    @classmethod
159
    def _nearest_power_of_two(cls, value):
160
        """ Return nearest power of 2. """
161
        return int(2 ** math.floor(math.log(value, 2)))
162
163
    @classmethod
164
    def _mask(cls, prefix):
165
        """ Compute the mask. """
166
        return cls.port_length - prefix
167
168
    @classmethod
169
    def _raw_upper_bound(cls, base, prefix):
170
        """ Compute a raw upper bound. """
171
        return base + (2 ** cls._mask(prefix)) - 1
172
173
    def _cidr_to_range(self, base, prefix):
174
        """ Transform a CIDR-like notation into a port range. """
175
        # Validates base and prefix values.
176
        if not self.port_min <= base <= self.port_max:
177
            raise ValueError("Port base out of bounds.")
178
        if not 1 <= prefix <= self.port_length:
179
            raise ValueError("CIDR-like prefix out of bounds.")
180
181
        # Disallow offsets in strict mode.
182
        if (self.strict and prefix != self.port_length
183
                and not self._is_power_of_two(base)):
184
            raise ValueError("Port base is not a power of two.")
185
186
        port_from = base
187
        port_to = self._raw_upper_bound(base, prefix)
188
        return [port_from, port_to]
189
190
    @property
191
    def bounds(self):
192
        """ Return lower and upper bounds of the port range. """
193
        return self.port_from, self.port_to
194
195
    @property
196
    def base(self):
197
        """ Alias to port_from, used as a starting point for CIDR notation. """
198
        return self.port_from
199
200
    @property
201
    def offset(self):
202
        """ Port base offset from its nearest power of two. """
203
        return self.base - self._nearest_power_of_two(self.base)
204
205
    @property
206
    def prefix(self):
207
        """ A power-of-two delta means a valid CIDR-like prefix. """
208
        # Check that range delta is a power of 2
209
        port_delta = self.port_to - self.port_from + 1
210
        if not self._is_power_of_two(port_delta):
211
            return None
212
        return self.port_length - int(math.log(port_delta, 2))
213
214
    @property
215
    def mask(self):
216
        """ Port range binary mask, based on CIDR-like prefix. """
217
        return self._mask(self.prefix) if self.prefix else None
218
219
    @property
220
    def cidr(self):
221
        """ Return components of the CIDR-like notation. """
222
        return self.base, self.prefix
223
224
    @property
225
    def is_single_port(self):
226
        """ Is the range a single port? """
227
        return True if self.port_from == self.port_to else False
228
229
    @property
230
    def is_cidr(self):
231
        """ Is the range can be expressed using a CIDR-like notation? """
232
        return True if self.prefix is not None else False
233