valid_hosts()   F
last analyzed

Complexity

Conditions 16

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
c 1
b 0
f 0
dl 0
loc 50
rs 2.7111

How to fix   Complexity   

Complexity

Complex classes like valid_hosts() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import re
2
3
import socket
4
5
from netaddr import iter_iprange
6
from netaddr.core import AddrFormatError
7
8
9
def is_ip(addr):
10
    """Determine if a string is really an ip, or a hostname instead.
11
12
    Args:
13
        addr (str): The ip address string to check
14
15
    Returns:
16
        bool: Whether or not `addr` is a valid ip.
17
    """
18
    if '.' not in addr:
19
        return False
20
    parts = addr.split('.')
21
    for part in parts:
22
        try:
23
            int(part)
24
        except ValueError:
25
            return False
26
    return True
27
28
29
def is_hostname(addr):
30
    """Determine if a string is a hostname.
31
32
    Based on https://en.wikipedia.org/wiki
33
        /Hostname#Restrictions_on_valid_hostnames
34
35
    Args:
36
        addr (str): The address string to check
37
38
    Returns:
39
        bool: Whether or not `addr` is a valid hostname.
40
    """
41
    if any([
42
        '_' in addr,
43
        addr.endswith('-'),
44
        is_ip(addr),
45
    ]):
46
        return False
47
    return True
48
49
50
def valid_hosts(formcls, field):
51
    """Validate a list of IPs (Ipv4) or hostnames using python stdlib.
52
53
    This is more robust than the WTForm version as it also considers hostnames.
54
55
    Comma separated values:
56
    e.g. '10.7.223.101,10.7.12.0'
57
    Space separated values:
58
    e.g. '10.223.101 10.7.223.102'
59
    Ranges:
60
    e.g. '10.7.223.200-10.7.224.10'
61
    Hostnames:
62
    e.g. foo.x.y.com, baz.bar.z.com
63
64
    :param formcls (object): The form class.
65
    :param field (str): The list of ips.
66
    """
67
    ip_re = re.compile(r'[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}')
68
    data = field.data
69
    if ',' in data:
70
        ips = [ip for ip in data.split(',') if ip]
71
    elif ' ' in data:
72
        ips = [ip for ip in data.split(' ') if ip]
73
    elif '-' in data and re.match(ip_re, data):
74
        try:
75
            start, end = data.split('-')
76
            ips = iter_iprange(start, end)
77
            ips = [str(ip) for ip in list(ips)]
78
        except ValueError:
79
            raise ValueError(
80
                'Invalid range specified. Format should be: '
81
                'XXX.XXX.XXX.XXX-XXX.XXX.XXX.XXX '
82
                '(e.g. 10.7.223.200-10.7.224.10)')
83
        except AddrFormatError as e:
84
            raise ValueError(e)
85
    else:
86
        # Just use the single ip
87
        ips = [data]
88
    # If any fails conversion, it is invalid.
89
    for ip in ips:
90
        # Skip hostnames
91
        if not is_ip(ip):
92
            if not is_hostname(ip):
93
                raise ValueError('Invalid hostname: "{}"'.format(ip))
94
            else:
95
                continue
96
        try:
97
            socket.inet_aton(ip)
98
        except socket.error:
99
            raise ValueError('Invalid IP: {}'.format(ip))
100