Автопровізіонінг Yealink T19 + динамічна адресна книга

Коли я прийшов працювати в цю компанію, у мене вже була деяка база з ip апаратів, кільком серверам з asterisk і нашліпкою у вигляді FreeBPX. Крім того, паралельно працювала аналогова АТС Samsung IDCS500 і загалом була основною системою зв'язку в компанії, ip телефонія працювала тільки для відділу продажів. І все б варилося так і далі, але одного прекрасного дня було дано указ перекладати всіх на IP телефонію, були обумовлені терміни, закуплено обладнання і план з переведення підприємства в 21 століття став втілюватися в життя.
Перше що починає турбувати в такій ситуації, це швидко наростаюча кількість телефонних апаратів, якими треба якось керувати, друге, що сильно турбувало була телефонна книга. Якщо з першим нам міг допомогти Endpoint Manager (який до речі випиляли з останніх версій FreePBX), то з книгою виникали деякі питання:

  • По-перше, як забезпечити її точність при постійній зміні дислокації/плинності користувачів?
  • По-друге, як повністю знеособити телефони. І чи не заповнювати щоразу ім'я контакту?

Завдання було цікаве, рішення не змусило себе довго чекати. Зараз я наведу повний лістинг, а потім розберемо по порядку.

from scapy.all import sniff
from scapy.layers.inet import IP
import mysql.connector
import ldap
import getpass
import tftpy
import requests
import os
import time
from string import replace

def conn_ldap(login):
    ad = ldap.initialize('ldap://***.local')
    ad.simple_bind_s('voip@***.local', 'password')
    basedn = 'OU=IT,DC=***,DC=LOCAL'
    basedn_user = 'OU=***,OU=***,DC=***,DC=LOCAL'
    scope = ldap.SCOPE_SUBTREE
    filterexp = "(&(sAMAccountName=" + login + ")(ObjectClass=person))"
    filterexp2 = "(&(ObjectClass=organizationUnit))"
    attrlist = ['cn']
    attrlist2 = ['OU']
    search = ad.search_s(basedn, scope, filterexp, attrlist)
    adname = search[0][1]['cn'][0].decode('utf-8')
    if adname == ' ':
        search = ad.search_s(basedn_user, scope, filterexp2, attrlist2)
        for i in range(1, len(search)+1):
            group = search[i][1]['ou'][0]
            basedn_user2 = 'OU='+group+','+basedn_user
            search = ad.search_s(basedn_user2, scope, filterexp, attrlist)
            adname = search[0][1]['cn'][0].decode('utf-8')
            if adname != ' ':
                return adname
        adname = search[0][1]['cn'][0].decode('utf-8')
    ad.unbind_s()
    return adname


def tftp_file_change(config,place,adname,current_account,current_account_password):

    client = tftpy.TftpClient("192.168.0.3", 69)
    client.download('template.cfg', place)
    fileread = open(place, 'r')
    line = fileread.readlines()
    fileread.close()
    line[5] = (('account.1.label = ').encode('utf-8') + adname.encode('utf-8') + 'n')
    line[2] = (('account.1.auth_name = ').encode('utf-8') + current_account.encode('utf-8') + 'n')
    line[3] = (('account.1.display_name = ').encode('utf-8') + current_account.encode('utf-8') + 'n')
    line[6] = (('account.1.password = ').encode('utf-8') + current_account_password[0][0] + 'n')
    filewrite = open(place, 'w')
    for i in line:
      filewrite.write(i)
    filewrite.close()
    print place
    print config
    client.upload(config,place)


def get_phone_inform(ipaddr):
    fileconf = requests.get('http://admin:admin@'+ipaddr+'/servlet?phonecfg=get[&accounts=1]')
    conf = fileconf.text.split('|')
    current_account = conf[2]
    return current_account


def sniff_frame():
    pcapf = sniff(count=1, timeout=70, filter="dst host 192.168.0.3 and port 5060")
    if len(pcapf) == 0:
        exit()
    frame = pcapf[0]
    macaddr = frame.src
    print macaddr[:8]
    if macaddr[:8] != '80:5e:c0':
        exit()
    ipaddr = frame[0][IP].src
    return macaddr, ipaddr


def conn_mysql(query,fquery,macaddr,qwery2):
    connect = mysql.connector.connect(host='192.168.0.3', database='voip', user='voip_wr', password='***')
    cursor = connect.cursor()
    cursor.execute(fquery)
    state = cursor.fetchall()
    state = bool(state[0][0])
    if state == True:
        cursor.execute(qwery2)
        connect.commit()
        connect.close()
    else:
        cursor.execute(query)
        connect.commit()
        connect.close()


def check_account(current_account):
    connect = mysql.connector.connect(host='192.168.0.3', database='asterisk', user='voip_wr', password='***')
    cursor = connect.cursor()
    qwery = 'select data from sip where id=' + current_account + ' and keyword="secret";'
    cursor.execute(qwery)
    password = cursor.fetchall()
    if password == ' ':
        exit()
    else:
        return password


if __name__ == '__main__':
    macaddr, ipaddr = sniff_frame()
    current_account = get_phone_inform(ipaddr)
    current_account_password = check_account(current_account)
    macaddr = macaddr.replace(':', '')
    ipaddr = ipaddr.decode('utf-8')
    adname = conn_ldap(getpass.getuser())
    query = 'INSERT INTO station (mac, ip, name, number) VALUES (' + '"' + macaddr + '",' + '"' + ipaddr + '",' + '"' + adname + '",' + '"' + get_phone_inform(ipaddr) + '"' + ')'
    qwery2 = 'UPDATE station SET ip=' + '"' + ipaddr + '"' + ', name=' + '"' + adname + '"' + ', number=' + '"' + get_phone_inform(ipaddr) + '"' + ' WHERE mac=' + '"' + macaddr + '"'
    fquery = 'SELECT EXISTS(SELECT mac FROM voip.station WHERE mac=' + '"' + macaddr + '")'
    query = query.encode('utf-8')
    fquery = fquery.encode('utf-8')
    config = macaddr + '.cfg'
    place = os.path.expanduser("~") + "" + "AppDataLocal" + config
    conn_mysql(query,fquery,macaddr,qwery2)
    tftp_file_change(config,place,adname,current_account,current_account_password)
    requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=AutoP')
    requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=Reboot')

Програма запускається на комп'ютері користувача та працює за умови, що комп'ютер підключено до мережі через телефон, оскільки Yealink T19 не вміє працювати як шлюз.

Для початку нам необхідно зрозуміти чи підключений? і який mac та ip має наш телефон.

def sniff_frame():
    pcapf = sniff(count=1, timeout=70, filter="dst host 192.168.0.3 and port 5060")
    if len(pcapf) == 0:
        exit()
    frame = pcapf[0]
    macaddr = frame.src
    print macaddr[:8]
    if macaddr[:8] != '80:5e:c0':
        exit()
    ipaddr = frame[0][IP].src
    return macaddr, ipaddr

Тут ми використовуємо функцію sniff з фраємворку scapy, за допомогою неї ми отримуємо заздалегідь певний udp пакет, чекаємо 70 секунд і якщо нічого не спіймали виходимо.

count=1, timeout=70, filter="dst host 192.168.0.3 and port 5060"

Далі переконуємось, що апарат дійсно Yealink і повертаємо необхідні значення (ip та mac).

За допомогою спеціального запиту з'ясовуємо поточний обліковий запис на телефоні. Для цього завантажується поточна конфігурація з телефону та розпарюється.

def get_phone_inform(ipaddr):
    fileconf = requests.get('http://admin:admin@'+ipaddr+'/servlet?phonecfg=get[&accounts=1]')
    conf = fileconf.text.split('|')
    current_account = conf[2]
    return current_account

З'ясовуємо пароль для цього облікового запису. Для цього звертаємось до таблиці asterisk.sip та в ній до поля data.

def check_account(current_account):
    connect = mysql.connector.connect(host='192.168.0.3', database='asterisk', user='voip_wr', password='***')
    cursor = connect.cursor()
    qwery = 'select data from sip where id=' + current_account + ' and keyword="secret";'
    cursor.execute(qwery)
    password = cursor.fetchall()
    if password == ' ':
        exit()
    else:
        return password

Ну і для фінального етапу підключаємося до ldap AD і за допомогою sAMAccountName, що отримується через функцію getpass.getuser() забираємо cn поточного користувача (у якому зазвичай міститься ПІБ користувача).

def conn_ldap(login):
    ad = ldap.initialize('ldap://***.local')
    ad.simple_bind_s('voip@***.local', 'password')
    basedn = 'OU=***,DC=***,DC=LOCAL'
    basedn_user = 'OU=***,OU=***,DC=***,DC=LOCAL'
    scope = ldap.SCOPE_SUBTREE
    filterexp = "(&(sAMAccountName=" + login + ")(ObjectClass=person))"
    filterexp2 = "(&(ObjectClass=organizationUnit))"
    attrlist = ['cn']
    attrlist2 = ['OU']
    search = ad.search_s(basedn, scope, filterexp, attrlist)
    adname = search[0][1]['cn'][0].decode('utf-8')
    if adname == ' ':
        search = ad.search_s(basedn_user, scope, filterexp2, attrlist2)
        for i in range(1, len(search)+1):
            group = search[i][1]['ou'][0]
            basedn_user2 = 'OU='+group+','+basedn_user
            search = ad.search_s(basedn_user2, scope, filterexp, attrlist)
            adname = search[0][1]['cn'][0].decode('utf-8')
            if adname != ' ':
                return adname
        adname = search[0][1]['cn'][0].decode('utf-8')
    ad.unbind_s()
    return adname

Підключаємося до заздалегідь створеної таблиці в бд (я була створена там же) і вносимо все те, що ми дізналися, а саме: ip, mac, ім'я користувача.

def conn_mysql(query,fquery,macaddr,qwery2):
    connect = mysql.connector.connect(host='192.168.0.3', database='voip', user='voip_wr', password='***')
    cursor = connect.cursor()
    cursor.execute(fquery)
    state = cursor.fetchall()
    state = bool(state[0][0])
    if state == True:
        cursor.execute(qwery2)
        connect.commit()
        connect.close()
    else:
        cursor.execute(query)
        connect.commit()
        connect.close()

На цьому можна було б зупинитися, адже ми вже створили динамічну адресну книгу, спитайте ви, але я пішов далі і прикрутив сюди ж автопровізійні апарати.

Для цього із заздалегідь налаштованого tftp сервера завантажується template конфігурація, в яку ми вносимо свої зміни і зберігаємо як mac.cfg. Тобто для Yealink існують два види конфігурації, одна глобальна, а друга застосовується до конкретного телефону і має бути виду mac_телефону.

Після всіх змін у файлі та збереження його назад на tftp сервер ми віддаємо команду телефону на провізіонінг та перезавантаження апарата.

def tftp_file_change(config,place,adname,current_account,current_account_password):

    client = tftpy.TftpClient("192.168.0.3", 69)
    client.download('template.cfg', place)
    fileread = open(place, 'r')
    line = fileread.readlines()
    fileread.close()
    line[5] = (('account.1.label = ').encode('utf-8') + adname.encode('utf-8') + 'n')
    line[2] = (('account.1.auth_name = ').encode('utf-8') + current_account.encode('utf-8') + 'n')
    line[3] = (('account.1.display_name = ').encode('utf-8') + current_account.encode('utf-8') + 'n')
    line[6] = (('account.1.password = ').encode('utf-8') + current_account_password[0][0] + 'n')
    filewrite = open(place, 'w')
    for i in line:
      filewrite.write(i)
    filewrite.close()
    print place
    print config
    client.upload(config,place)

requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=AutoP')
requests.get('http://admin:admin@'+ipaddr+'/cgi-bin/ConfigManApp.com?key=Reboot')

Після перезавантаження апарата ми отримуємо повне фіо на екрані телефону + завжди коректно заповнену адресну книгу в особі БД, далі залишається тільки прикрутити XML і PHP для динамічного відображення контенту. Таких прикладів маса є навіть у самого YEALINK.

PS: Для більшої масштабованості можна винести основні налаштування (змінні) в окремий файл.

Джерело: habr.com

Додати коментар або відгук