Creation d'un service alertsms sur zamok via rabbitmq

This commit is contained in:
Gabriel Detraz 2015-08-26 01:51:37 +02:00
parent 0adfe858cc
commit 79182f2b42
5 changed files with 97 additions and 0 deletions

7
utils/alertsms/README Normal file
View file

@ -0,0 +1,7 @@
# Daemon qui recupère les sms de rabbitmq et les envoie via gammu sur zamok
# Il récupère les sms en tant que user sms et dans la queue SMS
# Largement repris de crans ticket
# Les autres scripts n'ont plus qu'a peupler la file d'attente avec
# des dicts (numero de tel, contenu du msg)
# Un service gammu tourne sur zamok, ainsi qu'un service cranssms qui appelle daemon

344
utils/alertsms/alertsms.py Executable file
View file

@ -0,0 +1,344 @@
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
# Module pour la téléphonie
from gammu import smsd
from gammu.exception import GSMError
# Modules intrinsèques Python
import sys
import pwd
import os
import re
import socket
from email.message import Message
from email.parser import FeedParser
# Modules made in Cr@ns
from gestion.config import dns
from lc_ldap import shortcuts
import lc_ldap.filter2 as filter
#Exécuter le script sans essayer de contacter la clé 3G
if '--dry-run' in sys.argv[1:]:
DRY_RUN = True
print("Mode dry-run\n")
else:
DRY_RUN = False
# Exécuter le script en mode débug
if '--debug' in sys.argv[1:]:
DEBUG = True
print("Mode débug\n")
else:
DEBUG = False
#-------------------------------------------------------------------------------
# 0) Initialisation de LDAP,objets + fcts utiles
#-------------------------------------------------------------------------------
# Taille maximale du corps d'un message SMS
if '--unicode' in sys.argv[1:]:
MAX_CHAR = 70
else:
MAX_CHAR = 160
# Connexion à la base LDAP
ldap = shortcuts.lc_ldap_admin()
# Serveur boite aux lettres
MAILBOX_SERVER = 'zamok.crans.org'
if DEBUG:
log_file = open(os.getenv('HOME')+'/alertsms.log', mode='w')
# Serveurs pouvant distribuer les mails de manière légitime
TRUSTED_SERVERS = [ (
name.replace('freebox.crans.org','titanic.crans.org'),
name.replace('freebox.crans.org','titanic.crans.org').replace('.','.adm.',1),
{ 'ipv4' : '', 'ipv6' : '' }
) for name in dns.MXs.keys() ]
for _,server,ip_dict in TRUSTED_SERVERS:
list_ip = socket.getaddrinfo(server,None,0,0,socket.IPPROTO_TCP)
for _,_,_,_,ip in list_ip:
if ':' in ip[0]:
ip_dict['ipv6'] = ip[0]
else:
ip_dict['ipv4'] = ip[0]
# IP spéciales autorisées à envoyer des messages
SPECIAL_AUTH_IPv4 = []
SPECIAL_AUTH_IPv6 = []
# Expressions régulières pour le traitement du header des mails
catch_from_info = re.compile('from \[?(?P<sender>[a-zA-Z0-9.-]*)\]? \((?P<real_sender>[a-zA-Z0-9.-]*) ?\[((IPv6:(?P<ipv6>[0-9a-f:]*))|(?P<ipv4>[0-9.]*))\]\).*')
catch_by_info = re.compile('.*by (?P<who>[a-zA-Z0-9.]*) .*',flags=re.DOTALL)
def get_sender_info(line):
"""
Extrait les infos d'un élément Received du header
(prétendu nom de l'expéditeur, son nom d'après le DNS, son adresse IP)
"""
m = catch_from_info.match(line)
if m is None:
return (None,None,None,None)
return (m.group('sender'),m.group('real_sender'),m.group('ipv4'),m.group('ipv6'))
def get_all_sender_info(message):
"""
Extrait les infos de tous les éléments Received du header d'un mail
et en fait une liste (en conservant l'ordre des éléments)
"""
return [ get_sender_info(line) for line in message.get_all('Received') ]
def get_by_info(line):
"""
Extrait le nom du serveur ayant ajouté la ligne Received donnée
"""
m = catch_by_info.match(line)
if m is None:
return None
return m.group('who')
def get_all_by_info(message):
"""
Extrait le nom de tous les serveurs ayant ajouté une ligne Received
(en conservant l'ordre)
"""
return [ get_by_info(line) for line in message.get_all('Received') ]
def get_trusted_servers_v4():
"""
Renvoie la liste des serveurs de confiance avec leur IPv4
"""
return [(name,adm_name,ip_dict['ipv4']) for name,adm_name,ip_dict in TRUSTED_SERVERS ]
def get_trusted_servers_v6():
"""
Renvoie la liste des serveurs de confiance avec leur IPv6
"""
return [(name,adm_name,ip_dict['ipv6']) for name,adm_name,ip_dict in TRUSTED_SERVERS ]
if DEBUG:
log_file.writelines(['Phase 0 : OK\n'])
#-------------------------------------------------------------------------------
# 1) Lire le mail dans le flux stdin
#-------------------------------------------------------------------------------
# Ouverture de stdin
stream = sys.stdin
# Récupération du mail
mail = stream.readlines()
fp_mail = FeedParser()
fp_mail.feed("".join(mail))
msg = fp_mail.close()
# On le décortique
received_chain = get_all_sender_info(msg)
by_chain = get_all_by_info(msg)
mail_subject = msg['Subject']
mail_content = msg.get_payload().replace('\n',' ')
if DEBUG:
log_file.writelines(['Phase 1 : OK\n'])
#-------------------------------------------------------------------------------
# 2) Ce message est-il destiné à être envoyé par SMS ?
#-------------------------------------------------------------------------------
if '[SMS]' not in mail_subject:
if DEBUG:
log_file.writelines(["Message non destiné au service SMS"])
sys.exit(200)
if DEBUG:
log_file.writelines(['Phase 2 : OK\n'])
#-------------------------------------------------------------------------------
# 3) Analyse de la chaine de réception du message
#-------------------------------------------------------------------------------
# Il faut qu'il y a au moins 2 serveurs dans la chaîne (boite aux lettres + serveur de mail)
# Sinon, cela signifie a priori que le message à été déposé directement dans la boite
# Il faut aussi vérifier qui a distribué le mail
if len(received_chain) < 2:
if DEBUG:
log_file.writelines(["La chaine de réception n'est pas assez longue"])
sys.exit(300)
if by_chain[0] != MAILBOX_SERVER:
if DEBUG:
log_file.writelines(["La boîte aux lettres est suspecte :\
{0} au lieu de {1}".format(repr(by_chain[0]),MAILBOX_SERVER)])
sys.exit(301)
if DEBUG:
log_file.writelines(["Boîte aux lettres : " + MAILBOX_SERVER])
trusted_server = False
sender,real_sender,ipv4,ipv6 = received_chain[0]
if ipv4:
server = (sender,real_sender,ipv4)
trusted_server = server in get_trusted_servers_v4()
elif ipv6:
server = (sender,real_sender,ipv6)
trusted_server = server in get_trusted_servers_v6()
if not trusted_server:
if DEBUG:
log_file.writelines(["Le serveur de distribution du courrier est suspect"])
sys.exit(302)
if DEBUG:
log_file.writelines(["Serveur de distribution : " + repr(server) + "\n"])
# On fait confiance à la boîte aux lettres et au serveur de distribution
# Le serveur de distribution est le premier serveur Crans à recevoir le mail
# Au-delà, on a aucun contrôle : On est obligé de faire confiance
log_file.writelines(['Phase 3 : OK\n'])
#-------------------------------------------------------------------------------
# 4) Vérifier l'identité de l'expéditeur
#-------------------------------------------------------------------------------
# On récupère l'adresse IP de l'expéditeur
_,_,sender_ipv4,sender_ipv6 = received_chain.pop()
# S'agit-il d'un expéditeur spécial ?
special_auth = sender_ipv4 in SPECIAL_AUTH_IPv4 or sender_ipv6 in SPECIAL_AUTH_IPv6
pseudo = 'Special'
# Si ce n'est pas une adresse spéciale, on fait une recherche LDAP
if not special_auth:
if sender_ipv4:
sender_ip = sender_ipv4
f = filter.human_to_ldap('ipHostNumber=' + sender_ipv4)
elif sender_ipv6:
sender_ip = sender_ipv6
f = filter.human_to_ldap('ip6HostNumber=' + sender_ipv6)
else:
if DEBUG:
log_file.writelines(["Identification impossible : L'expéditeur n'a ni IPv4 ni IPv6"])
sys.exit(402)
sender = ldap.search(f)
# --> Si il n'y a aucun résultat, on quitte le script
if not sender:
if DEBUG:
log_file.writelines(['Aucun résultat pour l\'expéditeur dans la base LDAP'])
sys.exit(400)
sender = sender[0]
# --> Dans le cas contraire, on regarde qui c'est
if u'machineCrans' in [ objet.value for objet in sender['objectClass'] ]:
is_authorized = True # C'est une machine Crans
pseudo = sender['host'][0].value.split('.',1)[0]
elif u'machineFixe' in [ objet.value for objet in sender['objectClass'] ] \
or u'machineWifi' in [ objet.value for objet in sender['objectClass'] ]:
if u'Nounou' in [ droit.value for droit in sender.proprio()['droits'] ]:
is_authorized = True # C'est une nounou
pseudo = sender.proprio()['uid'][0].value
else:
is_authorized = False
else:
is_authorized = False
# On quitte le script si on a pas l'autorisation
if not is_authorized:
if DEBUG:
log_file.writelines(["Impossible d'envoyer le SMS (Autorisation refusée)"])
sys.exit(401)
if DEBUG:
log_file.writelines(['Phase 4 : OK\n'])
#-------------------------------------------------------------------------------
# 5) Contacter le démon sms
#-------------------------------------------------------------------------------
if not DRY_RUN:
try:
# On essaie de contacter le démon SMS
daemon = smsd.SMSD('/etc/gammu-smsdrc')
except GSMError:
# On quitte si l'initialisation a échoué
if DEBUG:
log_file.writelines(["Impossible de contacter le démon SMS"])
sys.exit(500)
if DEBUG:
log_file.writelines(['Phase 5 : OK\n'])
#-------------------------------------------------------------------------------
# 6) Ecrire le SMS
#-------------------------------------------------------------------------------
# On récupère l'utilisateur courant
user = pwd.getpwuid(os.getuid())[0]
# On trouve le numéro de l'adhérent associé dans la base LDAP
f = filter.human_to_ldap('uid='+user)
number = str(ldap.search(f)[0]['tel'][0])
# On fabrique une regex pour la forme du numéro de téléphone
tel_pattern = re.compile('^((336)|(337)|(06)|(07))[0-9]{8}$')
# On vérifie que ce soit un numéro de téléphone mobile valide
if not tel_pattern.match(number):
if DEBUG:
log_file.writelines(["Numéro du destinataire invalide"])
sys.exit(600)
# Si le numéro commence par l'indicatif 33, on rajoute le +
if number.startswith('33'):
number = '+' + number
# On écrit le message
text = (pseudo + u' : ' + mail_content)[0:MAX_CHAR-1]
message = {
'Text' : text,
'SMSC' : { 'Location' : 1 },
'Number' : number,
}
if DEBUG:
log_file.writelines([u"Destinataire : " + user])
log_file.writelines([u"Contenu du message : " + text + "\n"])
log_file.writelines(['Phase 6 : OK\n'])
#-------------------------------------------------------------------------------
# 7) L'envoyer
#-------------------------------------------------------------------------------
if not DRY_RUN:
try:
# On envoie le message
daemon.InjectSMS([message])
except GSMError:
if DEBUG:
log_file.writelines(["Le message n'a pas pu être placé dans la file d'attente"])
sys.exit(700)
if DEBUG:
log_file.writelines(['Phase 7 : OK\n'])

5
utils/alertsms/common.py Normal file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
PIDFILE = '/var/run/daemon.pid'
USER='gammu'
GROUP='adm'

16
utils/alertsms/config.py Normal file
View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import pika
import sys
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
from gestion import secrets_new as secrets
CREDS = pika.credentials.PlainCredentials('sms', secrets.get('rabbitmq_sms'), True)
PARAMS = pika.ConnectionParameters(host='rabbitmq.crans.org',
port=5671, credentials=CREDS, ssl=True)
QUEUE = "SMS"

69
utils/alertsms/daemon.py Executable file
View file

@ -0,0 +1,69 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Daemon d'envoie des sms par rabbitmq
# License GPL2
# Gabriel Détraz, pompé sur cransticket de Daniel Stan
from __future__ import print_function
import sys,os,pwd,grp
from gammu import smsd
import common
import pika
import json
import config
daemon = smsd.SMSD('/etc/gammu-smsdrc')
def run():
conn = pika.BlockingConnection(config.PARAMS)
ch = conn.channel()
ch.queue_declare(queue=config.QUEUE)
def callback(ch, method, properties, body):
print (" [x] Received %r" % (body,))
message = {
'Text': json.loads(body)['sms'],
'SMSC' : { 'Location' : 1 },
'Number' : json.loads(body)['numero'],
}
daemon.InjectSMS([message])
ch.basic_consume(callback, queue=config.QUEUE, no_ack=True)
ch.start_consuming()
conn.close()
# fork en arrière plan + pidfile
if __name__ == "__main__":
if '-fg' in sys.argv:
run()
exit()
# do the UNIX double-fork magic, see Stevens' "Advanced
# Programming in the UNIX Environment" for details (ISBN 0201563177)
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError, e:
print("fork #1 failed: %d (%s)" % (e.errno, e.strerror),file=sys.stderr)
sys.exit(1)
# decouple from parent environment
os.chdir("/") #don't prevent unmounting....
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent, print eventual PID before
#print "Daemon PID %d" % pid
open(common.PIDFILE, 'w').write("%d" % pid)
sys.exit(0)
except OSError, e:
print("fork #2 failed: %d (%s)" % (e.errno, e.strerror),file=sys.stderr)
sys.exit(1)
# start the daemon main loop
run()