#!/usr/bin/python3
#coding=utf8

import os
import fnmatch
import time
import re
import fcntl
from xml.dom.minidom import getDOMImplementation
from subprocess import getoutput, getstatusoutput
from copy import copy
from sys import argv
import subprocess
from datetime import datetime
from glob import glob
import syslog
import struct
from ctypes import *


class SNDefs:
    PREFIX = '/opt/secretnet/'
    PATH_ADD_ENABLE = '/tmp/uevent_add_enable'
    PATH_UDEV_RULE = '/etc/udev/rules.d/99-snlsp.rules'
    PATH_STAT = '/proc/snsecure/stats'

#
class SNDebug:
    debug_log_file = '/opt/secretnet/var/sn_uevent.log'
    debug_err_file = '/opt/secretnet/var/sn_uevent_err.log'

    def __init__(self, mode):
        if mode == 0:
            SNDebug.DEBUG = 0
        else:
            SNDebug.DEBUG = 1
            now = datetime.now()
            SNDebug.logfile = open(self.debug_log_file, "a")
            SNDebug.log("\n\nSNDebug log.  %s\n" % now.strftime("%H:%M:%S"))
            SNDebug.errfile = open(self.debug_err_file, "a")
            SNDebug.err("\n\nSNDebug errors.  %s\n" % now.strftime("%H:%M:%S"))

    @staticmethod
    def log(text):
        if SNDebug.DEBUG == 1:
            SNDebug.logfile.write(text)

    @staticmethod
    def err(text):
        if SNDebug.DEBUG == 1:
            SNDebug.errfile.write(text)


# функция блокировки для предотвращения одновременной работы нескольких экземпляров скрипта
def uevent_lock():
    try:
        fd = os.open(SNDefs.PREFIX + 'var/tmp/uevent_lock', os.O_RDWR | os.O_CREAT )
    except:
        return False

    try:
        fcntl.flock(fd, fcntl.LOCK_EX)
        return True
    except:
        return False


class FsHelpers:

    @staticmethod
    def readline(path):
        f = open(path, 'r')
        line = f.readline()
        f.close()
        return line.strip()

    @staticmethod
    def readlines(path):
        f = open(path, 'r')
        lines = f.readlines()
        f.close()
        return lines

    @staticmethod
    def find_files(directory, pattern):
        for root, dirs, files in os.walk(directory):
            for basename in files:
                if fnmatch.fnmatch(basename, pattern):
                    filename = os.path.join(root, basename)
                    yield filename

    @staticmethod
    def get_first_content(dev_dir, attr, depth):
        for i in range(0, depth):
            for f in FsHelpers.find_files(dev_dir, attr):
                return FsHelpers.readline(f)
            dev_dir = os.path.dirname(dev_dir)

    @staticmethod
    def get_first_content_ex(dev_dir, attr):
        while(1):
            f = dev_dir + "/" + attr
            if os.path.exists(f):
                return FsHelpers.readline(f)

            dev_dir = os.path.dirname(dev_dir)
            if dev_dir is None or dev_dir == '/':
                break
        return None

    @staticmethod
    def get_first_content_modalias(dev_dir):
        retVal = ""
        while(1):
            f = dev_dir + "/modalias"
            if os.path.exists(f):
                text = FsHelpers.readline(f)
                tp = text.split(":")
                if tp[0] == "usb" or tp[0] == "ieee1394":
                    return text
                elif tp[0] == "scsi":
                    retVal = text
            dev_dir = os.path.dirname(dev_dir)
            if dev_dir is None or dev_dir == '/':
                break
        return retVal

class UdevDeviceInfo:

    def __parse_entry(self, entry):
        t, pair = entry.split(': ')
        if t == 'E':
            key, value = pair.split('=')
            self.__info[key] = value

    def __parse_dev(self, dev):
        lines = dev.split('\n')
        self.name = lines[0].strip()
        self.__info['name'] = self.name  # for debug
        for entry in lines:
            try:
                self.__parse_entry(entry)
            except:
                pass            # format is not key=value

    def __repr__(self):
        return '<UdevDeviceInfo instance ' + self.name + ' ' + str(self.__info) + '>'

    def __init__(self, raw):
        self.__info = dict()
        self.__parse_dev(raw)

    def __getitem__(self, item):
        if self.has_key(item):
            return self.__info[item]
        else:
            return ""

    def get_short_serial(self):
        s = self.__info['ID_SERIAL']
        m = self.__info['ID_MODEL']
        return s[s.find(m) + len(m) + 1:]

    def has_key(self, key):
        return key in self.__info


class UdevDeviceAttr:

    def __parse_entry(self, entry):
        attr, value = entry.split('==')
        attr = attr.replace('ATTR{', '')
        attr = attr.replace('ATTRS{', '')
        attr = attr.replace('}', '')
        attr = attr.strip()
        value = value.replace('"', '')
        self.__info[attr] = value

    def __parse_dev(self, dev):
        lines = dev.split('\n')
        for entry in lines:
            try:
                self.__parse_entry(entry)
            except:
                pass            # format is not attr==value

    def __init__(self, raw):
        self.__info = dict()
        self.__parse_dev(raw)

    def __getitem__(self, item):
        if self.has_key(item):
            return self.__info[item]
        else:
            return ""

    def has_key(self, key):
        return key in self.__info


class Sysfs:

    @staticmethod
    def get_dev_id(path):
        return FsHelpers.readline(os.path.dirname(path) + '/dev')

    @staticmethod
    def get_child_devs(path):
        main_dev_id = Sysfs.get_dev_id(path)
        devs = []
        for path in FsHelpers.find_files(os.path.dirname(path), 'dev'):
            dev_id = FsHelpers.readline(path)
            devname = '/dev/' + os.path.basename(os.path.dirname(path))
            if dev_id != main_dev_id:
                devs += [(dev_id, devname)]
        return devs

    @staticmethod
    def get_modalias(path):
        return FsHelpers.get_first_content_modalias(os.path.realpath(path)).split(':')[0]

    @staticmethod
    def get_type(path):
        return Sysfs.get_modalias(path).split(':')[0]

    @staticmethod
    def get_vid(path):
        return FsHelpers.get_first_content(os.path.realpath(path), 'idVendor', depth=5)

    @staticmethod
    def get_pid(path):
        return FsHelpers.get_first_content(os.path.realpath(path), 'idProduct', depth=5)

    @staticmethod
    def get_vendor(path):
        return FsHelpers.get_first_content(os.path.realpath(path), 'vendor', depth=1)

    @staticmethod
    def get_product(path):
        return FsHelpers.get_first_content(os.path.realpath(path), 'model', depth=1)

    @staticmethod
    def get_serial(path):
        return FsHelpers.get_first_content(os.path.realpath(path), 'serial', depth=5)

    @staticmethod
    def get_usb_class(path):
        return FsHelpers.get_first_content(os.path.realpath(path), 'bInterfaceClass', depth=5)

    @staticmethod
    def find_block_devices():
        return getoutput('find /sys/block/ |grep device').split()


class Device:
    serv_attrs = ['type', 'devtype', 'devname', 'devpath',
                  'dev-id', 'bus-id', 'class-id']
    dev_attrs = ['idVendor', 'idProduct', 'vendor', 'product', 'serial', 'usbClass', 'label',
                 'subsysIdVendor', 'subsysIdProduct', 'revision', 'dev_class',
                 'interface_type', 'slot', 'mac', 'ifiindex', 'control']

    def __init__(self):
        for attr in self.serv_attrs + self.dev_attrs:
            exec('self.' + attr.replace('-', '_') + ' = None')

    def append_to(self, doc):
        node = doc.createElement("node")

        for attr in self.serv_attrs:
            var = eval('self.' + attr.replace('-', '_'))
            if var:
                node.setAttribute(attr, var)

        for name in self.dev_attrs:
            var = eval('self.' + name)
            if var:
                attr = doc.createElement(name)
                attr.setAttribute('value', var)
                node.appendChild(attr)

        doc.documentElement.appendChild(node)


class Uniq:
    SDEV_UNIQ_MAX = 255

    keys = ['idVendor', 'idProduct', 'vendor', 'product', 'serial',
            'dev_type', 'subsysIdVendor', 'subsysIdProduct', 'revision',
            'interface_type', 'dev_class', 'slot', 'mac', 'class-id', 'bus-id']

    def __init__(self):
        self.attrs = dict()
        for key in self.keys:
            self.attrs[key] = None

    def create(self):
        uniq = str()
        if self.attrs['mac']:
            mac = self.attrs['mac'].replace(':', '')
        dev_type = self.attrs['dev_type']
        bus_id = self.attrs['bus-id']
        if dev_type is None:
            if bus_id is None or bus_id != 'usb':
                for key in self.keys:
                    if self.attrs[key] != None and key != 'bus-id':
                        uniq += ' ' + key + ':' + self.attrs[key]
            else:
                uniq += ' ' + 'idVendor' + ':' + self.attrs['idVendor']
                uniq += ' ' + 'idProduct' + ':' + self.attrs['idProduct']
                if self.attrs['serial']:
                    uniq += ' ' + 'serial' + ':' + self.attrs['serial']
        elif dev_type == 'pci':
            uniq = '{}{:0>6}{:0>4}{:0>4}{:0>4}{:0>4}{:0>2}{}'.format(
                self.attrs['class-id'][0], self.attrs['dev_class'], self.attrs['idVendor'],
                self.attrs['idProduct'], self.attrs['subsysIdVendor'],
                self.attrs['subsysIdProduct'], self.attrs['revision'], self.attrs['slot'])
        elif dev_type == 'usb':
            uniq = '{}{:0>4}{:0>4}'.format(
                self.attrs['class-id'][0], self.attrs['idVendor'], self.attrs['idProduct'])
            if self.attrs['serial']:
                uniq += self.attrs['serial']
            if self.attrs['product']:
                uniq += self.attrs['product']
            if self.attrs['vendor']:
                uniq += self.attrs['vendor']
        elif dev_type == 'virtual':
            uniq = '{}{}'.format(self.attrs['class-id'][0], mac, )
            if self.attrs['interface_type']:
                uniq += self.attrs['interface_type']
        elif dev_type == 'other':
            uniq = '{}{}'.format(self.attrs['class-id'][0], mac)

        uniq = uniq.strip()[0:Uniq.SDEV_UNIQ_MAX]
        return uniq

class NetList:
    EthernetType = 1
    Ieee1394Type = 24
    IrdaType = 783
    EthernetClassName = "ethernet"
    Ieee1394ClassName = "firewire"
    IrdaClassName = "irda"
    WiFiClassName = "wifi"
    BluetoothClassName = "bluetooth"
    SupportedTypes = [EthernetType, Ieee1394Type, IrdaType]
    SupportedBusInfo = ['tun', 'tap']

    @staticmethod
    def is_supported_net(net_path):
        try:
            with open(os.path.join(net_path, 'type')) as f:
                net_type = int(f.read())
                if net_type not in NetList.SupportedTypes:
                    return None
                else:
                    return net_type
        except IOError:
            return None

    @staticmethod
    def get_pci_attr(dev_path, file_attr, offset, length):
        try:
            with open(os.path.join(dev_path, file_attr)) as f:
                return f.read()[len('0x'):].strip().lower()
        except:
            pass

        try:
            with open(os.path.join(dev_path, 'config'), 'rb') as f:
                config = f.read()
                _bytes = struct.unpack('{}s'.format(length), config[offset:offset+length])
                return ''.join('{:02x}'.format(x) for x in reversed(_bytes[0]))
        except Exception:
            return ''


    @staticmethod
    def get_label(net_dev_info, mac):
        database_product = net_dev_info.__getitem__('ID_MODEL_FROM_DATABASE')
        database_vendor = net_dev_info.__getitem__('ID_VENDOR_FROM_DATABASE')
        if database_product and database_vendor:
            label = database_vendor + ' ' + database_product
        elif database_product:
            label = database_product + ' ' + mac
        elif database_vendor:
            label = database_vendor + ' ' + mac
        else:
            id_oui = net_dev_info.__getitem__('ID_OUI_FROM_DATABASE')
            if id_oui:
                label = id_oui + ' ' + mac
            else:
                label = net_dev_info.__getitem__('INTERFACE') + ' ' + mac
        return label


    def __init__(self):
        self.devices = list()

        for net_path in glob("/sys/class/net/*"):
            net_path = os.path.realpath(net_path)
            net_type = NetList.is_supported_net(net_path)
            if not net_type:
                continue

            dev = Device()

            dev.devname = net_path

            net_dev_info = UdevDeviceInfo(getoutput('udevadm info --path=' + net_path))
            interface = net_dev_info.__getitem__('INTERFACE')
            dev.dev_id = interface
            dev.control = interface
            dev.ifiindex = net_dev_info.__getitem__('IFINDEX')

            dev_path = net_path
            while os.path.isdir(os.path.join(dev_path, 'device')):
                dev_path = os.path.join(dev_path, 'device')

            subsystem = os.path.basename(os.path.realpath(os.path.join(dev_path, 'subsystem')))

            try:
                with open(os.path.join(net_path, 'address')) as f:
                    dev.mac = f.read().strip().lower()

                if subsystem == 'pci':
                    dev.vendor = net_dev_info.__getitem__('ID_VENDOR_FROM_DATABASE')
                    dev.product = net_dev_info.__getitem__('ID_MODEL_FROM_DATABASE')
                    if not dev.vendor:
                        dev.label = NetList.get_label(net_dev_info, dev.mac)

                    dev.dev_class = NetList.get_pci_attr(dev_path, 'class', 9, 3)
                    dev.idVendor = NetList.get_pci_attr(dev_path, 'vendor', 0, 2)
                    dev.idProduct = NetList.get_pci_attr(dev_path, 'device', 2, 2)
                    dev.subsysIdVendor = NetList.get_pci_attr(dev_path, 'subsystem_vendor', 0x2C, 2)
                    dev.subsysIdProduct = NetList.get_pci_attr(dev_path,'subsystem_device', 0x2E, 2)
                    dev.revision = NetList.get_pci_attr(dev_path, 'revision', 8, 1)

                    bus_path = os.path.abspath(os.path.join(net_path ,"../.."))
                    while os.path.basename(os.path.realpath(os.path.join(bus_path, 'subsystem'))) \
                            == 'pci':
                        dev.slot = os.path.basename(bus_path)
                        bus_path = os.path.dirname(bus_path)

                    dev.type = "pci"
                elif subsystem == 'usb':
                    usb_dev = os.path.join(os.path.realpath(dev_path), '../')
                    with open(os.path.join(usb_dev, 'idVendor')) as f:
                        dev.idVendor = f.read().strip().lower()
                    with open(os.path.join(usb_dev, 'idProduct')) as f:
                        dev.idProduct = f.read().strip().lower()

                    try:
                        with open(os.path.join(usb_dev, 'manufacturer')) as f:
                            dev.vendor = f.read()[:-1]
                    except IOError:
                        pass

                    try:
                        with open(os.path.join(usb_dev, 'product')) as f:
                            dev.product = f.read()[:-1]
                    except IOError:
                        pass

                    try:
                        with open(os.path.join(usb_dev, 'serial')) as f:
                            dev.serial = f.read()[:-1]
                    except IOError:
                        pass

                    dev.type = "usb"

                    if not dev.vendor:
                        dev.label = NetList.get_label(net_dev_info, dev.mac)
                elif subsystem == 'net':
                    dev.type = 'virtual'
                    dev.interface_type = ''
                    if os.path.isdir(os.path.join(dev_path, 'bridge')):
                        dev.interface_type = 'bridge'
                    else:
                        lines = getoutput('ethtool -i ' + interface).split('\n')
                        try:
                            bus_info = [line for line in lines if 'bus-info' in line][0].split(':')[1].strip()
                            if bus_info in NetList.SupportedBusInfo:
                                dev.interface_type = bus_info
                        except:
                            pass

                    dev.label = NetList.get_label(net_dev_info, dev.mac + ' ' + dev.interface_type)
                else:
                    dev.type = "other"
                    dev.label = NetList.get_label(net_dev_info, dev.mac)
            except IOError:
                continue

            dev.bus_id = "net"
            if net_type == NetList.EthernetType:
                dev.class_id = NetList.EthernetClassName
                devtype = net_dev_info.__getitem__('DEVTYPE')
                if 'wlan' == devtype:
                    dev.class_id = NetList.WiFiClassName
                elif 'bluetooth' == devtype:
                    dev.class_id = NetList.BluetoothClassName
            elif net_type == NetList.Ieee1394Type:
                dev.class_id = NetList.Ieee1394ClassName
            elif net_type == NetList.IrdaType:
                dev.class_id = NetList.IrdaClassName

            self.devices += [dev]


class DeviceList:

    TokensParam = (
        ('0a89', '0002'), # Guardant ID
        ('0a89', '0020'), # RuToken S
        ('0a89', '0025'), # RuToken Lite
        ('0a89', '0030'), # RuToken ECP
        ('0dc3', '0802'), # Athena V3
        ('0dc3', '1004'), # Athena IIIe
        ('0dc3', '1108'), # Athena Bio Combo
        ('0dc3', '1008'), # Athena CCID
        ('24dc', '010f'), # JaCarta Flash
        ('24dc', '0101'), # JaCarta PKI
        ('24dc', '0102'), # JaCarta LT
        ('24dc', '0401'), # JaCarta Webpass
        ('24dc', '0402'), # JaCarta-2 SE
        ('24dc', '0406'), # JaCarta SF/GOST
        ('24dc', '0501'), # JaCarta U2F
        ('072f', '90de'), # ESMART Token
        ('072f', 'b100'), # ESMART Token (ACS)
        ('2ce4', '7479'), # ESMART Token (Micron)
        ('072f', '90cc'), # ACR38U
        ('2bb1', '0f10'), # vdToken
    )

    BusIdBlock = {'ata', 'scsi', 'ide'}
    BlockMajorsExclude = {7, 0, 1, 2} # loop, unnamed, mem, ramdisk
    CharMajors = {99, 180, 189} # ppdev, usb, usb_device

    def __init__(self, no_net = False, no_real_dev = False):
        self.find_in_dev(no_net, no_real_dev)

    def get_xml(self):
        impl = getDOMImplementation()
        newdoc = impl.createDocument(None, "devices", None)
        for dev in self.devices:
            dev.append_to(newdoc)
        return '\n'.join(node.toprettyxml() for node in newdoc.childNodes)

    def get(self):
        return self.devices, self.block_usb_storages

    @staticmethod
    def usb_class_by_itf(itf):
        usbClass = itf[0:2]
        usbSubClass = itf[2:4]
        usbProtocol = itf[4:6]

        if usbClass == "02":
            # communication:
            class_id = "usb_net"

        elif usbClass == "03":
            class_id = "usb_hid"

        elif usbClass == "06":
            class_id = "usb_image"

        elif usbClass == "07":
            class_id = "usb_printer"

        elif usbClass == "08":
            class_id = "usb_storage"

        elif usbClass == "09":
            class_id = "none"       # exclude hub

        elif usbClass == "0a":
            # data:
            class_id = "usb_net"

        elif usbClass == "0b":
            # smartcard:
            class_id = "usb_uid"

        elif usbClass == "e0":
            # wireless:
            if usbSubClass == "01" and (usbProtocol == "01" or usbProtocol == "04"):
                class_id = "usb_bluetooth"
            else:
                class_id = "usb_net"

        elif usbClass == "ef":
            # miscellaneous:
            if usbSubClass == "01":
                class_id = "usb_phone"
            else:
                class_id = "usb_other"

        elif usbClass == "fe":
            # application:
            if usbSubClass == "02":
                class_id = "usb_net"		# IRDA
            else:
                class_id = "usb_other"

        else:
            class_id = "usb_other"

        return class_id

    # ищем устройства в /dev/char and /dev/block,
    # получаем описания на них через 'udevadm info'
    # и заносим результаты в self.devices
    def find_in_dev(self, no_net = False, no_real_dev = False):
        self.devices = list()

        if not no_net:
            self.devices.extend(NetList().devices)

        if no_real_dev:
            return

        # список путей и имен блочных usb_storage устройств
        self.block_usb_storages = list()

        dev_id_list = list()

        for pnp_dev in os.listdir('/sys/bus/pnp/devices'):
            pnp_path = '/sys/bus/pnp/devices/' + pnp_dev + '/tty/'
            if not os.path.exists(pnp_path):
                continue
            list_dir = os.listdir(pnp_path)
            if len(list_dir) != 1:
                continue
            with open(pnp_path + list_dir[0] + '/dev') as f:
                dev_id = f.read()[:-1]
                dev_id_list.append((dev_id, os.path.join('/dev/char', dev_id)))

        for block_dev in os.listdir('/dev/block'):
            major = int(block_dev.split(':')[0])
            if major not in DeviceList.BlockMajorsExclude:
                dev_id_list.append((block_dev, os.path.join('/dev/block', block_dev)))

        for char_dev in os.listdir('/dev/char'):
            major = int(char_dev.split(':')[0])
            if major in DeviceList.CharMajors:
                dev_id_list.append((char_dev, os.path.join('/dev/char', char_dev)))

        for node, path in dev_id_list:
            if (re.match ("^\.", node)):
                continue

            udev_info_text = getoutput('udevadm info --name=' + path)
            dev_info = UdevDeviceInfo(udev_info_text)

            subsystem = dev_info.__getitem__('SUBSYSTEM')
            bus_id = dev_info.__getitem__('ID_BUS')
            devname = dev_info.__getitem__('DEVNAME')
            devtype = dev_info.__getitem__('DEVTYPE')
            devpath = dev_info.__getitem__('DEVPATH')
            id_type = dev_info.__getitem__('ID_TYPE')
            serial = dev_info.__getitem__('ID_SERIAL_SHORT')
            idVendor = dev_info.__getitem__('ID_VENDOR_ID')
            idProduct = dev_info.__getitem__('ID_MODEL_ID')
            vendor = dev_info.__getitem__('ID_VENDOR')
            product = dev_info.__getitem__('ID_MODEL')
            port = '0x0'

            if subsystem == 'tty' or subsystem == 'ppdev':
                # последовательные и параллельные порты:
                udev_attr_text = getoutput('udevadm info --attribute-walk --name=' + path)
                dev_attr = UdevDeviceAttr(udev_attr_text)
                if subsystem == 'tty' and devpath.find('pnp') > 0:
                    port = dev_attr.__getitem__('port')
                else:
                    port = dev_attr.__getitem__('id')

            class_id = 'none'
            usbClass = 'none'

            if subsystem == 'block':
                #	block
                if bus_id in DeviceList.BusIdBlock:
                    if id_type == 'disk':
                        if devtype == 'disk' or devtype == 'partition':
                            class_id = 'physical'   # HDD / partition
                    if devtype == 'disk' and 'cd' in id_type:
                        class_id = 'optical'        # CD/DVD drive
                    bus_id =  'local'
                elif bus_id == 'usb':
                    class_id = 'usb_storage'        # любой носитель через USB-интерфейс
                    if devtype == 'disk' or devtype == 'partition':
                        self.block_usb_storages += [(devpath, node)]

                if class_id == 'none':
                    SNDebug.log("*** %s: block, %s - %s - %s\n" %
                        (node, devtype, id_type, bus_id))

            elif subsystem == 'usb':
                #	USB
                interfaces_raw = dev_info.__getitem__('ID_USB_INTERFACES')

                # может выглядеть так: 'ID_USB_INTERFACES=:030000:060101:ff0000:'
                interfaces = interfaces_raw.strip(":")
                interfaces = interfaces.split(":")

                for itf in interfaces:
                    class_id = DeviceList.usb_class_by_itf(itf)
                    if class_id != "usb_other":
                        break

                # не все токены имеют usb-класс 0x0b
                # поэтому проверяем их по списку поддерживаемых токенов и считывателей
                token_param = (idVendor, idProduct)
                if token_param in DeviceList.TokensParam:
                    class_id = 'usb_uid'

            elif subsystem == 'tty' or subsystem == 'ppdev':
                    if (port != '0x0' and port != ''):
                        bus_id = "local"
                        product = 'port ' + port
                        devtype = "port"
                        if subsystem == 'tty':
                            class_id = "tty"
                        else:
                            class_id = "parport"

            else:
                # остальное пока не анализируем:
                SNDebug.log("%s subsystem: %s\n" % (node, subsystem))

            # готовим описание устройства:
            if class_id != "none":
                serial = serial.replace('-', '_')
                vendor = vendor.replace('-', '_')
                product = product.replace('-', '_')

                dev = Device()
                if subsystem == "block":
                    dev.type = "block"
                else:
                    dev.type = "char"
                dev.bus_id = bus_id
                dev.class_id = class_id
                dev.devtype = devtype

                dev.devpath = devpath
                if bus_id == "usb":
                     dev.control = '/sys' + devpath + '/authorized'
                if class_id == "physical" or class_id == 'optical':
                    dev.control = '/sys' + devpath + '/device/delete'

                dev.dev_id = node
                dev.devname = devname

                uniq = Uniq()
                if serial != "":
                    uniq.attrs['serial'] = serial
                    dev.serial = serial
                if idVendor != "":
                    uniq.attrs['idVendor'] = idVendor
                    dev.idVendor = idVendor
                if idProduct != "":
                    uniq.attrs['idProduct'] = idProduct
                    dev.idProduct = idProduct
                if vendor != "":
                    uniq.attrs['vendor'] = vendor
                    dev.vendor = vendor
                if product != "":
                    uniq.attrs['product'] = product
                    dev.product = product

                uniq.attrs['class-id'] = class_id
                uniq.attrs['bus-id'] = bus_id;

                dev.usbClass = usbClass
                if subsystem == 'tty' or subsystem == 'ppdev':
                    dev.uniq = port
                else:
                    dev.uniq = uniq.create()

                SNDebug.log("%s: type '%s', devtype '%s'," % (node, dev.type, dev.devtype))
                SNDebug.log(" dev_id '%s', bus_id '%s', class_id '%s'\n" % (dev.dev_id, dev.bus_id ,dev.class_id))
                SNDebug.log(" uniq = '%s'\n" % dev.uniq)

                self.devices += [dev]


class Rule:
    def __init__(self, rule, inh):
        self.rule = rule            # правило
        self.inh = inh              # наследование

class RuleEntry:
    def __init__(self, id, acl, dc, uniq = ''):
        self.id = id
        self.acl = acl
        self.dc = dc
        self.uniq = uniq

    def get_rule(self, rule_type):
        if rule_type == 'acl':
            return self.acl
        return self.dc


class DevCtlRules:
    bus_rules_path = SNDefs.PREFIX + '/var/devctl.bus'
    dev_rules_path = SNDefs.PREFIX + '/var/devctl.dev'

    acl_keys = ['acl-usr', 'acl-grp', 'acl-all', 'dc']

    # парсер devctl.bus
    def __prepare_bus(self, all_bus):
        # для найденных в devctl.bus объектов выделяем
        # классификатор object_id, наборы правил acl и dc и флаги наследования inh_acl и inh_dc
        # найденное сохраняем их в масиве bus_rules[]
        for bus in all_bus:
            acl = str()
            dc = ' --dc=not-control'                # пока может отсутствовать
            inh_acl = 0
            inh_dc = 0
            class_id = ''
            vid = ''
            pid = ''
            options = bus.split()
            for option in options:
                key, value = option.split('=')
                if key in self.acl_keys:
                    if key == 'dc':
                        dc = ' --' + option         # контроль подключения
                    else:
                        acl += ' --' + option       # разграничение доступа
                # элементы классификатора:
                elif key == 'bus-id':
                    bus_id = value
                elif key == 'class-id':
                    class_id = value
                elif key == 'idVendor':
                    vid = value
                elif key == 'idProduct':
                    pid = value
                # наследование:
                elif key == 'inh-acl':
                    if value == "enabled":
                        inh_acl = 1
                elif key == 'inh-dc':
                    if value == "enabled":
                        inh_dc = 1
            # классификатор:
            object_id = bus_id + ':' + class_id + ':' + vid + ':' + pid
            acl_rule = Rule(acl, inh_acl)
            dc_rule = Rule(dc, inh_dc)
            self.bus_rules += [RuleEntry(object_id, acl_rule, dc_rule)]
        #
        SNDebug.log("Objects:\n")
        for bus in self.bus_rules:
            SNDebug.log("id = %s, acl = '%s' (inh = %d), dc = '%s' (inh = %d)\n" %
                (bus.id, bus.acl.rule, bus.acl.inh, bus.dc.rule, bus.dc.inh))


    # ищем правило в родительском объекте по классификатору object_id
    # тип правила 'acl'/'dc' задается параметром rule_type
    def find_parent(self, object_id, rule_type):
        SNDebug.log("object_id = '%s'\n" % object_id)
        bus_id, class_id, vid, pid = object_id.split(':')
        if vid != "":           # vid/pid в устройстве могут отсутствовать
            # модель:
            id = object_id
            for object in self.bus_rules:
                if id == object.id:
                    SNDebug.log("found_parent[%s] id for model = %s\n" % (rule_type, id))
                    perm = object.get_rule(rule_type)   # правила + наследование
                    if perm.inh == 0:                   # наследование?
                        return perm.rule                # правила
        if class_id != "":      # пока нет поддержки классов в плагине
            # класс:
            id = bus_id + ':' + class_id + "::"
            for object in self.bus_rules:
                if id == object.id:
                    SNDebug.log("found_parent[%s] id for class = %s\n" % (rule_type, id))
                    perm = object.get_rule(rule_type)   # правила + наследование
                    if perm.inh == 0:                   # наследование?
                        return perm.rule                # правила
        # шина:
        id = bus_id + ":::"
        for object in self.bus_rules:
            if id == object.id:
                SNDebug.log("found_parent[%s] id for bus = %s\n" % (rule_type, id))
                perm = object.get_rule(rule_type)       # правила + наследование
                return perm.rule                        # правила
        SNDebug.err("*** fail to find parent object for %s" % object_id)
        return None


    # парсер devctl.dev
    def __prepare_dev(self, all_dev):
        # для найденных в devctl.dev устройств выделяем
        # классификатор object_id, наборы правил acl и dc и флаги наследования inh_acl и inh_dc;
        # попутно формируем идентификатор устройства uniq;
        # все сохраняем в масиве dev_rules[]
        for dev in all_dev:
            acl = str()
            dc = ' --dc=not-control'                    # пока может отсутствовать
            uniq = str()
            inh_acl = 0
            inh_dc = 0
            class_id = ''
            vid = ''
            pid = ''
            product = ''
            uniq = Uniq()
            options = dev.split()
            for option in options:
                key, value = option.split('=')
                if key in self.acl_keys:
                    if key == 'dc':
                        dc = ' --' + option             # контроль подключения
                    else:
                        acl += ' --' + option           # разграничение доступа
                elif key in uniq.keys:
                    uniq.attrs[key] = value             # идентификатор
                # элементы классификатора:
                if key == 'bus-id':
                    bus_id = value
                elif key == 'class-id':
                    class_id = value
                elif key == 'idVendor':
                    vid = value
                elif key == 'idProduct':
                    pid = value
                elif key == 'product':
                    product = value
                # наследование:
                elif key == 'inh-acl':
                    if value == "enabled":
                        inh_acl = 1
                elif key == 'inh-dc':
                    if value == "enabled":
                        inh_dc = 1

            # собираем классификатор:
            object_id = bus_id + ':' + class_id + ':' + vid + ':' + pid

            # сразу вычисляем наследование acl:
            if inh_acl == 1:
                parent_rule = self.find_parent(object_id, 'acl')
                if parent_rule != None:
                    acl = parent_rule

            # сразу вычисляем наследование dc:
            if inh_dc == 1:
                parent_rule = self.find_parent(object_id, 'dc')
                if parent_rule != None:
                    dc = parent_rule

            # заносим в массив:
            self.dev_rules += [RuleEntry(object_id, Rule(acl, 0), Rule(dc, 0), uniq.create())]

        SNDebug.log("Devices:\n")
        for dev in self.dev_rules:
            SNDebug.log("id = %s, '%s', '%s', uniq = '%s'\n" %
                (dev.id, dev.acl.rule, dev.dc.rule, dev.uniq))


    def __init__(self):
        self.bus_rules = list()
        self.__prepare_bus(FsHelpers.readlines(self.bus_rules_path))
        self.dev_rules = list()
        self.__prepare_dev(FsHelpers.readlines(self.dev_rules_path))


class DevCtl:
    devctl_path = SNDefs.PREFIX + '/sbin/devctl'

    @staticmethod
    def run(args):
        SNDebug.log("devctl %s\n" % args)
        status, output = getstatusoutput(DevCtl.devctl_path + ' ' + args)
        return status

    @staticmethod
    def bus_reset():
        return DevCtl.run('dev rst')

    @staticmethod
    def device_add(rule):
        return DevCtl.run('dev add ' + rule)

    @staticmethod
    def bus_set(rule):
        return DevCtl.run('bus set ' + rule)

    # получение значения политики safe_secdev:
    @staticmethod
    def get_safe_secdev():
        stat_file = open(SNDefs.PATH_STAT, 'r')
        lines = stat_file.readlines()
        stat_file.close()
        safe_secdev = 'disabled'
        for i in range(len(lines)):
            line = lines[i].replace('\n', '')
            line = line.strip()
            if line == 'safe_secdev:':
                safe_secdev = lines[i + 1].replace('\n', '').strip()
                break
        return safe_secdev


class UserspaceHandler:

    def __init__(self, cmd, argv):
        self.argv = argv
        if cmd == "help":
            self.__help()
        elif cmd == "query-xml":
            no_net = False
            no_real_dev = False
            if '-n' in argv:
                no_net = True
            if '-r' in argv:
                no_real_dev = True
            self.__query_xml(no_net, no_real_dev)
        elif cmd == "reset":
            self.__reset()
        elif cmd == "settle":
            self.__settle()
        elif cmd == "add":
            if os.path.isfile(SNDefs.PATH_ADD_ENABLE):
                self.__settle()

    def __help(self):
        print(("Usage: %s <command>" % self.argv[0]))
        print("""
Commands:
       help          - show help and exit
       query-xml     - query devices information in xml
       reset         - reset all the rules
       settle        - reset and rescan all the devices
       add           - rescan after new device connection""")

    def __query_xml(self, no_net = False, no_real_dev = False):
        print(DeviceList(no_net, no_real_dev).get_xml())

    def __reset(self):
        rules = DevCtlRules()
        devices = DeviceList()
        DevCtl.bus_reset()

    def __settle(self):

        if not os.path.isfile(SNDefs.PATH_ADD_ENABLE):
            add_enable = open(SNDefs.PATH_ADD_ENABLE, 'w')
            add_enable.write('1')
            add_enable.close()

        rules = DevCtlRules()
        devices, block_usb = DeviceList().get()
        DevCtl.bus_reset()

        for dev in devices:
            if dev.bus_id == 'net':
                continue

            # блочное usb_storage пропускается, так как его не видно из плагина
            # и его правила недостоверны:
            if dev.class_id == 'usb_storage' and dev.type == 'block':
                continue

            isInRules = False
            SNDebug.log("\ndev.uniq = '%s'\n" % dev.uniq)
            for dev_rule in rules.dev_rules:
                if dev.uniq == dev_rule.uniq:
                    # нашли устройство в списке эффективных правил:
                    acl = dev_rule.acl.rule
                    dc = dev_rule.dc.rule
                    isInRules = True
                    break;

            if not isInRules:
                # пытаемся найти правило для объекта:
                if dev.idVendor == None:
                    vid = ""
                else:
                    vid = dev.idVendor
                if dev.idProduct == None:
                    pid = ""
                else:
                    pid = dev.idProduct
                if dev.product == None:
                    product = ""
                else:
                    product = dev.product
                object_id = dev.bus_id + ':' + dev.class_id + ':' + vid + ':' + pid
                acl = rules.find_parent(object_id, 'acl')
                if acl == None:
                    acl = " --acl-all=undef"
                dc = rules.find_parent(object_id, 'dc')
                if dc == None:
                    dc = " --dc=not-control"
                SNDebug.log("    -  not InRules:\n")

            rule = (' --dev-id=' + dev.dev_id +
                ' --name=' + dev.devname +
                ' --bus-id=' + dev.bus_id +
                ' --class-id=' + dev.class_id +
                ' --uniq="' + dev.uniq + '"' +
                acl + dc)
            DevCtl.device_add(rule)
            if dev.class_id == 'usb_storage' and dev.type == 'char':
                # правило для char usb_storage отправляется и соответствующим блочным устройствам,
                # если таковые имеются:
                for b_devpath, b_devid in block_usb:
                    pos = b_devpath.find(dev.devpath)
                    if pos == 0:
                        rule = (' --dev-id=' + b_devid +
                            ' --name=' + dev.devname +
                            ' --bus-id=' + dev.bus_id +
                            ' --class-id=' + dev.class_id +
                            ' --uniq="' + dev.uniq + '"' +
                            acl + dc)
                        DevCtl.device_add(rule)

        net_dc_rule = rules.find_parent('net:::', 'dc')

        # загрузить в ядро правила на классы сетевых интерфейсов
        for bus_rule in rules.bus_rules:
            bus_id, class_id, vid, pid = bus_rule.id.split(':')
            if bus_id != 'net' or class_id == '' or vid != '' or pid != '':
                continue
            if bus_rule.dc.inh == 1:
                dc = net_dc_rule
            else:
                dc = bus_rule.dc.rule

            rule = (' --bus-id=' + bus_id +
                    ' --class-id=' + class_id +
                    dc)
            DevCtl.bus_set(rule)

        # загрузить в ядро правила на сетевые интерфейсы
        for dev_rule in rules.dev_rules:
            bus_id, class_id, vid, pid = dev_rule.id.split(':')
            if bus_id != 'net':
                continue
            SNDebug.log("dev.uniq = '%s'\n" % dev_rule.uniq)
            rule = (' --bus-id=' + bus_id +
                    ' --class-id=' + class_id +
                    ' --uniq="' + dev_rule.uniq + '"' +
                    dev_rule.acl.rule + dev_rule.dc.rule)
            SNDebug.log("    - isInRules:\n")
            DevCtl.device_add(rule)


if __name__ == '__main__':
    if uevent_lock() == False:
        os._exit(os.EX_OK)

    start = datetime.now()

    debug_mode = 0
    if  len(argv) > 2:
        if argv[2] == '-v':
            debug_mode = 1
    SNDebug(debug_mode)

    if len(argv) > 1:
        if debug_mode == 1:
            SNDebug.log("uevent %s\n" % argv[1])
        UserspaceHandler(argv[1], argv)
    else:
        UserspaceHandler('help', argv)

    if debug_mode == 1:
        now = datetime.now()
        delta = now - start
        msec = delta.microseconds // 1000
        SNDebug.log("\n duration = %d msec\n" % msec)

