crans_utils.py 11.8 KB
Newer Older
1 2 3 4 5
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# CRANS_UTILS.PY-- Utils for Cr@ns gestion
#
6 7 8
# Copyright (c) 2010-2013, Cr@ns <roots@crans.org>
# Authors: Antoine Durand-Gasselin <adg@crans.org>
#         Pierre-Elliott Bécue <becue@crans.org>
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
#   notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
#   notice, this list of conditions and the following disclaimer in the
#   documentation and/or other materials provided with the distribution.
# * Neither the name of the Cr@ns nor the names of its contributors may
#   be used to endorse or promote products derived from this software
#   without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

33 34 35 36 37 38 39
import calendar
import netaddr
import re
import time
import smtplib
import sys
import os
40
import base64
41
import collections
42
import hashlib
43
import ldap.filter
44 45 46 47

if '/usr/scripts' not in sys.path:
    sys.path.append('/usr/scripts')
from gestion import config
48
from unicodedata import normalize
49
import subprocess
50
from netifaces import interfaces, ifaddresses, AF_INET
51

52 53
DEVNULL = open(os.devnull, 'w')

54 55 56 57 58 59
def find_rid_plage(rid):
    """Trouve la plage du rid fourni"""
    for (tp, plages) in config.rid_primaires.iteritems():
        if isinstance(plages, list):
            for begin, end in plages:
                if begin <= rid <= end:
60
                    return tp, config.rid_primaires[tp][0]
61 62 63 64 65 66 67 68 69 70 71 72 73 74
        else:
            (begin, end) = plages
            if begin <= rid <= end:
                return tp, (begin, end)
    else:
        return 'Inconnu', (0, 0)

def find_ipv4_plage(ipv4):
    """Trouve la plage de l'ipv4 fournie"""
    for (tp, plage) in config.NETs_primaires.iteritems():
        for sousplage in map(netaddr.IPNetwork, plage):
            if ipv4 in sousplage:
                return tp, sousplage

Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
75 76
def ip4_of_rid(rid):
    """Convertit un rid en son IP associée"""
77 78
    # Au cas où
    rid = int(rid)
79 80
    if rid == -1:
        return u""
81

82 83
    net, plage = find_rid_plage(rid)
    if net == 'Inconnu':
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
84
        raise ValueError("Rid dans aucune plage: %d" % rid)
85

86 87
    if net == 'special':
        try:
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
88
            return netaddr.IPAddress(config.rid_machines_speciales[rid])
89
        except KeyError:
90 91 92 93
            raise ValueError(u"Machine speciale inconnue: %d" % rid)
    try:
        return netaddr.IPAddress(netaddr.IPNetwork(config.NETs[net][0]).first + rid - plage[0])
    except KeyError:
94
        return u""
95

96 97
def rid_of_ip4(ipv4):
    """Convertit une ipv4 en rid, si possible"""
98 99 100
    if ipv4 == "":
        return -1

101 102 103 104 105 106 107
    # Est-ce une machine spéciale ?
    for (rid, ip) in config.rid_machines_speciales.iteritems():
        if str(ipv4) == ip:
            return rid

    # Le cas non-échéant, on va devoir faire de la deep NETs inspection
    realm, sousplage = find_ipv4_plage(ipv4)
108

109
    return config.rid[realm][0][0] + int(ipv4 - sousplage.first)
110

Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
111
def prefixev6_of_rid(rid):
112 113
    """
    L'ip de sous-réseau privé d'une machine. L'adhérent en fait ce qu'il veut, mais c'est la machine
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
114
    associée au rid qui est responsable du traffic.
115 116 117

    Cette fonction retourne l'ip de début de ce sous-réseau.
    """
118
    # Au cas où
119 120
    rid = int(rid)

121 122
    net, plage = find_rid_plage(rid)
    if net == 'Inconnu':
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
123
        raise ValueError("Rid dans aucune plage: %d" % rid)
124

125 126
    # adherents-v6 ou wifi-adh-v6, we don't care
    return netaddr.IPAddress(netaddr.IPNetwork(config.prefix['adherents-v6'][0]).first + 2**64*rid)
127

Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
128
def ip6_of_mac(mac, rid):
129
    """
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
130
    Retourne la bonne ipv6 de la machine en fonction de sa mac et de son rid.
131
    """
132 133
    # Au cas où
    rid = int(rid)
134 135
    if rid == -1:
        return u""
136

137 138
    net, plage = find_rid_plage(rid)
    if net == 'Inconnu':
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
139
        raise ValueError("Rid dans aucune plage: %d" % rid)
140

141 142
    # En théorie, format_mac est inutile, car on ne devrait avoir
    # que des mac formatées.
143 144 145 146
    mac = format_mac(mac)
    if mac == u'<automatique>':
        return u''
    mac = mac.replace(u':', u'')
147 148

    # hex retourne un str, donc on concatène, suivant la RFC
149
    euid64v6 = hex(int(mac[:2], 16)^0b00000010) + mac[2:6] + u'fffe' + mac[6:12]
150

151
    # adherents-v6 ou wifi-adh-v6, we don't care
152 153 154
    if net != "special":
        return netaddr.IPAddress(netaddr.IPNetwork(config.prefix[net][0]).first + int(euid64v6, 16))
    else:
155
        return netaddr.IPAddress(config.ipv6_machines_speciales[rid])
156

157 158
def strip_accents(a):
    """ Supression des accents de la chaîne fournie"""
159
    res = normalize('NFKD', a).encode('ASCII', 'ignore')
160
    return unicode(res)
161

162
def strip_spaces(a, by=u'_'):
163
    """ Suppression des espaces et des apostrophes"""
164
    return a.replace(u' ', by).replace(u"'", u'')
165 166 167 168 169 170

def mailexist(mail):
    """Vérifie si une adresse mail existe ou non grace à la commande
    vrfy du serveur mail """

    mail = mail.split('@', 1)[0]
171 172 173 174 175 176 177
#    try:
    s = smtplib.SMTP('smtp.adm.crans.org')
    s.putcmd("vrfy", mail)
    r = s.getreply()[0] in [250, 252]
    s.close()
#    except:
#        raise ValueError(u'Serveur de mail injoignable')
178 179 180

    return r

181
def format_ldap_time(tm):
182
    """Formatage des dates provenant de la base LDAP
183 184 185 186 187 188 189
    Transforme la date YYYYMMDDHHMMSS.XXXXXXZ (UTC)
    en date DD/MM/YY HH:MM (local)"""
    tm_st = time.strptime(tm.split('.')[0], "%Y%m%d%H%M%S") # struct_time UTC
    timestamp = calendar.timegm(tm_st)
    tm_st = time.localtime(timestamp) # struct_time locale
    return time.strftime("%d/%m/%Y %H:%M", tm_st)

190
def format_mac(mac):
191
    """ Formatage des adresses mac
192 193 194
    Transforme une adresse pour obtenir la forme xx:xx:xx:xx:xx:xx
    Retourne la mac formatée.
    """
195 196 197
    mac = unicode(mac).lower()
    if mac == u'<automatique>':
        return mac
198
    mac = netaddr.EUI(mac)
199 200
    if not mac:
        raise ValueError(u"MAC nulle interdite\nIl doit être possible de modifier l'adresse de la carte.")
201
    return unicode(str(mac).replace('-', ':'))
202

203
def format_tel(tel):
204
    """Formatage des numéros de téléphone
205 206 207 208 209
    Transforme un numéro de téléphone pour ne contenir que des chiffres
    (00ii... pour les numéros internationaux)
    Retourne le numéro formaté.
    """
    tel_f = tel.strip()
210 211
    if tel_f.startswith(u"+"):
        tel_f = u"00" + tel_f[1:]
212 213
    if u"(0)" in tel_f:
        tel_f = tel_f.replace(u"(0)", u"")
214
    # \D = non-digit
215
    tel_f = re.sub(r'\D', '', tel_f)
216
    return unicode(tel_f)
217

218 219 220
def validate_name(value, more_chars=""):
    """Valide un nom: ie un unicode qui contient lettres, espaces et
    apostrophes, et éventuellement des caractères additionnels"""
221
    if re.match("^[A-Za-z0-9]([-' %s]?[A-Za-z0-9]?)*$" % more_chars,
222
                normalize('NFKD', value).encode('ASCII', 'ignore')):
223
        return unicode(value)
224
    else:
225
        raise ValueError("Nom invalide (%r)" % value)
226

227 228 229 230
def process_status(pid):
    """
    Vérifie l'état du processus pid
    """
231 232
    try:
        os.getpgid(int(pid))
233
        return True
234 235
    except OSError:
        return False
236 237 238 239 240

def escape(chaine):
    """Renvoie une chaîne échapée pour pouvoir la mettre en toute sécurité
       dans une requête ldap."""
    return ldap.filter.escape_filter_chars(chaine)
241 242


243 244 245
def hash_password(password, salt=None, longueur=4):
    if longueur < 4:
        raise ValueError("salt devrait faire au moins 4 octets")
246
    if salt is None:
247
        salt = os.urandom(longueur)
248
    elif len(salt) < 4:
249 250
        raise ValueError("salt devrait faire au moins 4 octets")

251
    return '{SSHA}' + base64.b64encode(hashlib.sha1(password + salt).digest() + salt)
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299

def decode_subjectAltName(data):
    from pyasn1.codec.der import decoder
    from pyasn1_modules.rfc2459 import SubjectAltName
    altName = []
    sa_names = decoder.decode(data, asn1Spec=SubjectAltName())[0]
    for name in sa_names:
        name_type = name.getName()
        if name_type == 'dNSName':
            altName.append(unicode(name.getComponent()))
# Cacert met des othername, du coup, on ignore juste
#        else:
#            raise ValueError("Seulement les dNSName sont supporté pour l'extension de certificat SubjectAltName (et pas %s)" % name_type)
    return altName


def fetch_cert_info(x509):
        # Attention, pour les X509req, la vertion de openssl utilisé et celle du fichier /usr/scripts/lib/python2.7/site-packages/pyOpenSSL-0.14-py2.7.egg
        # /usr/scripts/python.sh met le path comme il faut, sinon, il faudrait faire quelque chose comme :
        # for file in os.listdir('/usr/scripts/lib/python2.7/site-packages/'):
        #     if file.endswith(".egg"):
        #         sys.path.insert(2, '/usr/scripts/lib/python2.7/site-packages/%s' % file)
        # sys.path.insert(2, '/usr/scripts/lib/python2.7/site-packages/')
        import OpenSSL
        data = {}
        data['subject'] = dict(x509.get_subject().get_components())
        if isinstance(x509, OpenSSL.crypto.X509):
            data['issuer'] = dict(x509.get_issuer().get_components())
            data['start'] = int(time.mktime(time.strptime(x509.get_notBefore(), '%Y%m%d%H%M%SZ')))
            data['end'] = int(time.mktime(time.strptime(x509.get_notAfter(), '%Y%m%d%H%M%SZ')))
            data['serialNumber'] = unicode(int(x509.get_serial_number()))
        data['extensions'] = collections.defaultdict(list)
        def do_ext(data, ext):
            ext_name = ext.get_short_name()
            if ext_name == 'subjectAltName':
                data['extensions'][ext_name] = decode_subjectAltName(ext.get_data())
            elif ext_name == 'extendedKeyUsage':
                data['extensions'][ext_name].append(str(ext))
            else:
                data['extensions'][ext_name] = str(ext)
        if isinstance(x509, OpenSSL.crypto.X509):
            for i in range(0, x509.get_extension_count()):
                ext = x509.get_extension(i)
                do_ext(data, ext)
        else:
            for ext in x509.get_extensions():
                do_ext(data, ext)
        return data
300 301 302 303 304 305 306 307 308 309 310


def ip4_addresses():
    """Renvois la liste des ipv4 de la machine physique courante"""
    ip_list = []
    for interface in interfaces():
        if interface!='lo' and AF_INET in ifaddresses(interface).keys():
            for link in ifaddresses(interface)[AF_INET]:
                ip_list.append(link['addr'])
    return ip_list

311
def extract_tz(thetz):
312 313
    abstz = 100*abs(thetz)
    if thetz == 0:
314
        return u"Z"
315
    else:
316
        return u"%s%04d" % ("+"*(thetz < 0) + "-"*(thetz > 0), abstz)
317

318
def to_generalized_time_format(stamp):
319 320 321 322 323 324 325 326
    """Converts a timestamp (local) in a generalized time format
    for LDAP.

     * stamp : float value
     * output : a string without the dot second

    """

327
    return u"%s%s" % (time.strftime("%Y%m%d%H%M%S", time.localtime(stamp)), extract_tz(time.altzone/3600))
328

329
def from_generalized_time_format(gtf):
330 331 332 333 334 335 336
    """Converts a GTF stamp to unix timestamp

     * gtf : a generalized time format resource without dotsecond
     * output : a float value

    """
    return time.mktime(time.strptime(gtf.split("-", 1)[0].split("+", 1)[0].split('Z', 1)[0], "%Y%m%d%H%M%S"))