Overblog Tous les blogs Top blogs Technologie & Science Tous les blogs Technologie & Science
Editer l'article Suivre ce blog Administration + Créer mon blog
MENU
Aquila Networks : Sécurité & Réseaux Étudiant en Master SSI à l'ESMT

Enregistrement des appels Asterisk + Interface Web

El Hadji M. Diakhate
 Sommaire
  1. Objectifs pédagogiques
  2. Architecture du système
  3. Prérequis et installation des dépendances
  4. Étape 1 — Création de la base de données MySQL
  5. Étape 2 — Scripts Python (conversion + stockage)
  6. Étape 3 — Configuration Asterisk (extensions.conf)
  7. Étape 4 — File d'attente et traitement automatique (cron)
  8. Étape 5 — Interface Web PHP
  9. Étape 6 — Tests et validation
  10. Résultat final
Objectifs pédagogiques

À la fin de ce TP, vous serez capable de :

  • Configurer l'enregistrement des appels dans Asterisk avec MixMonitor
  • Stocker les métadonnées des appels dans une base MySQL
  • Convertir automatiquement les fichiers WAV en MP3
  • Développer une interface Web PHP sécurisée
  • Lire les fichiers audio directement depuis le navigateur
  • Gérer un historique d'appels avec filtres et pagination
  • Sécuriser l'accès aux enregistrements (auth HTTP Basic)
Architecture du système

Le système s'articule autour de 5 composants qui travaillent ensemble :


Fichiers du projet

Fichier     Rôle
 
/etc/asterisk/extensions.conf Dialplan Asterisk — MixMonitor + extension h
/var/lib/asterisk/recording/db_config.py   Configuration MySQL Python
/var/lib/asterisk/recording/post_record.py  Conversion WAV→MP3 et insertion MySQL
/var/lib/asterisk/recording/process_queue.sh       Processeur de la file d'attente (cron)
/var/www/html/recordings/config.php Configuration MySQL PHP + auth
/var/www/html/recordings/index.php Interface Web principale
/var/www/html/recordings/audio.php  Serveur audio sécurisé (streaming MP3)
/tmp/rec_queue.txt  File d'attente des enregistrements à traiter

 

 

 

 

 

 

 
   
  

Prérequis et installation des dépendances

Avant de commencer, installez les paquets nécessaires :

Terminal — root@aquila

# Dépendances système
apt-get update
apt-get install -y lame inotify-tools php-mysql

# Module Python MySQL
pip3 install mysql-connector-python --break-system-packages

# Vérifications
lame --version       # Doit afficher : LAME 64bits version 3.100
php -v               # Doit afficher : PHP 8.x
python3 -c "import mysql.connector; print('OK')"  # Doit afficher : OK

# Créer les dossiers nécessaires
mkdir -p /var/spool/asterisk/monitor
mkdir -p /var/lib/asterisk/recording
chown -R asterisk:asterisk /var/spool/asterisk/monitor
chmod 775 /var/spool/asterisk/monitor

# Créer la file avec les bonnes permissions
touch /tmp/rec_queue.txt
chmod 777 /tmp/rec_queue.txt
chown asterisk:asterisk /tmp/rec_queue.txt

# Ajouter www-data au groupe asterisk (pour lire les MP3)
usermod -aG asterisk www-data
systemctl restart apache2


Création de la base de données MySQL

Créez le fichier SQL puis exécutez-le :

 

# mysql -u root -p

sql
-- Créer la base de données
CREATE DATABASE IF NOT EXISTS asterisk_recordings
    CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE asterisk_recordings;

-- Table principale des enregistrements
CREATE TABLE IF NOT EXISTS recordings (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    uniqueid    VARCHAR(64)  NOT NULL DEFAULT '',
    calldate    DATETIME     NOT NULL,
    caller      VARCHAR(50)  NOT NULL DEFAULT '',
    callee      VARCHAR(50)  NOT NULL DEFAULT '',
    context     VARCHAR(50)  NOT NULL DEFAULT '',
    duration    INT          NOT NULL DEFAULT 0,
    wav_file    VARCHAR(255) DEFAULT NULL,
    mp3_file    VARCHAR(255) DEFAULT NULL,
    status      ENUM('pending','done','error') DEFAULT 'pending',
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_calldate (calldate),
    INDEX idx_caller   (caller),
    INDEX idx_callee   (callee)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Utilisateur dédié avec droits limités
CREATE USER IF NOT EXISTS 'asterisk_rec'@'localhost'
    IDENTIFIED BY 'Esmt@2025!';
GRANT SELECT, INSERT, UPDATE ON
    asterisk_recordings.recordings TO 'asterisk_rec'@'localhost';
FLUSH PRIVILEGES;
 

# Vérification


mysql -u asterisk_rec -p'Esmt@2025!' asterisk_recordings \
  -e "SHOW TABLES;"
# Doit afficher : recordings

L'utilisateur asterisk_rec n'a que les droits SELECT, INSERT, UPDATE — jamais DROP ou DELETE. C'est une bonne pratique de sécurité.


Scripts Python — conversion WAV→MP3 et stockage MySQL


2.1 — Fichier de configuration (db_config.py)


/var/lib/asterisk/recording/db_config.py


python
DB_CONFIG = {
    "host":     "localhost",
    "user":     "asterisk_rec",
    "password": "Esmt@2025!",
    "database": "asterisk_recordings",
    "charset":  "utf8mb4",
}

AUDIO_DIR = "/var/spool/asterisk/monitor"
LOG_FILE  = "/var/log/asterisk/recording_errors.log"
LAME_BIN  = "/usr/bin/lame"
2.2 — Script principal (post_record.py)


Ce script est appelé avec 6 arguments : fichier WAV, caller, callee, durée, uniqueid, contexte.

/var/lib/asterisk/recording/post_record.py


python
import sys, os, subprocess, logging
from datetime import datetime

sys.path.insert(0, "/var/lib/asterisk/recording")
from db_config import DB_CONFIG, AUDIO_DIR, LOG_FILE, LAME_BIN

logging.basicConfig(filename=LOG_FILE, level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s")

# Lecture des arguments
def get_arg(idx, default=""):
    return sys.argv[idx].strip() if len(sys.argv) > idx else default

WAV_FILE = get_arg(1)
CALLER   = get_arg(2, "inconnu")
CALLEE   = get_arg(3, "inconnu")
BILLSEC  = int(get_arg(4, "0"))
UNIQUEID = get_arg(5, "")
CONTEXT  = get_arg(6, "")

def convert_to_mp3(wav_path):
    mp3_path = wav_path.replace(".wav", ".mp3")
    subprocess.run([LAME_BIN, "--silent", "-b", "32", "-m", "m",
                    wav_path, mp3_path], check=True, timeout=60)
    os.remove(wav_path)
    logging.info(f"WAV converti → {mp3_path}")
    return mp3_path

def insert_db(wav_file, mp3_file, status="done"):
    import mysql.connector
    conn = mysql.connector.connect(**DB_CONFIG)
    cur  = conn.cursor()
    cur.execute("""
        INSERT INTO recordings
            (uniqueid, calldate, caller, callee, context,
             duration, wav_file, mp3_file, status)
        VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)
    """, (UNIQUEID, datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
           CALLER, CALLEE, CONTEXT, BILLSEC,
           os.path.basename(wav_file) if wav_file else None,
           os.path.basename(mp3_file) if mp3_file else None, status))
    conn.commit(); cur.close(); conn.close()

if __name__ == "__main__":
    if not WAV_FILE or not os.path.exists(WAV_FILE):
        logging.warning("Aucun fichier WAV fourni — appel ignoré.")
        sys.exit(0)
    try:
        mp3 = convert_to_mp3(WAV_FILE)
        insert_db(WAV_FILE, mp3)
        logging.info(f"OK | caller={CALLER} callee={CALLEE} dur={BILLSEC}s")
    except Exception as e:
        logging.error(f"ERREUR : {e}")
        insert_db(WAV_FILE, None, status="error")

 

Permissions
bash
chown asterisk:asterisk /var/lib/asterisk/recording/*.py
chmod +x /var/lib/asterisk/recording/post_record.py

 

# Test rapide

python3 /var/lib/asterisk/recording/post_record.py
cat /var/log/asterisk/recording_errors.log
# Doit afficher : WARNING Aucun fichier WAV fourni — appel ignoré.

 


Configuration Asterisk — extensions.conf
 

extensions complet

; ============================================================
; Contexte appels internes
; ============================================================
[from-internal]

; --- 1000 et 1001 fusionnés dans _1XXX (comportement identique) ---
; [REC] Pattern _1XXX gère désormais 1000 et 1001 avec enregistrement
exten => _1XXX,1,NoOp(Appel interne vers ${EXTEN})
 same => n,Set(CALLED_NUM=${EXTEN})
 same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
 same => n,MixMonitor(${RECFILE}.wav,b)
 same => n,Dial(PJSIP/${EXTEN})
 same => n,Hangup()

exten => 1441,1,Goto(message,1441,1)

exten => _2XXX,1,NoOp(Appel de A vers B : ${EXTEN})
 same => n,Set(CALLED_NUM=${EXTEN})
 same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
 same => n,MixMonitor(${RECFILE}.wav,b)
 same => n,Dial(PJSIP/${EXTEN}@trunk-to-b,30,tT)
 same => n,Hangup()

exten => 5081,1,Goto(ivr-esmt,s,1)

; [REC] Appels sortants GSM enregistrés
exten => _X.,1,NoOp(Appel sortant GSM : ${EXTEN})
 same => n,Set(CALLED_NUM=${EXTEN})
 same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
 same => n,MixMonitor(${RECFILE}.wav,b)
 same => n,Dial(PJSIP/${EXTEN}@goip)
 same => n,Hangup()

exten => 3000,1,Goto(svi-ia,3000,1)

; [REC] Extension h — post-traitement au raccrochage
exten => h,1,NoOp(Raccrochage — enregistrement en cours de traitement)
 same => n,GotoIf($["${RECFILE}" = ""]?end)
 same => n,Set(REC_BILLSEC=${CDR(billsec)})
 same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-internal" >> /tmp/rec_queue.txt)
 same => n(end),NoOp(Fin h from-internal)


; ============================================================
; SVI Intelligent IA — extension 3000
; ============================================================
[svi-ia]
exten => 3000,1,Answer()
 same => n,AGI(googletts.agi,"Bienvenue dans votre assistant administratif intelligent de l'ESMT",fr,any)
 same => n,AGI(googletts.agi,"Posez votre question après le signal",fr,any)
 same => n,Record(/var/spool/asterisk/tmp/question.wav,3,30)
 same => n,AGI(svi_ia.agi)
 same => n,Playback(/var/spool/asterisk/tmp/reponse)
 same => n,Hangup()


; ============================================================
; Appels entrants depuis le site B
; ============================================================
[from-trunk-b]
exten => _1XXX,1,NoOp(Appel reçu de B vers A : ${EXTEN})
 same => n,Set(CALLED_NUM=${EXTEN})
 same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
 same => n,MixMonitor(${RECFILE}.wav,b)
 same => n,Dial(PJSIP/${EXTEN},20,tT)
 same => n,Hangup()

; [REC] Extension h pour from-trunk-b
exten => h,1,NoOp(Raccrochage trunk-b)
 same => n,GotoIf($["${RECFILE}" = ""]?end)
 same => n,Set(REC_BILLSEC=${CDR(billsec)})
 same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-trunk-b" >> /tmp/rec_queue.txt)
 same => n(end),NoOp(Fin h from-trunk-b)


; ============================================================
; Rappel étudiant (inchangé)
; ============================================================
[student-callback]
exten => s,1,NoOp(=== Rappel etudiant Ext:${STUDENT_EXT} Keyword:${STUDENT_KEYWORD} ===)
 same => n,Answer()
 same => n,Wait(1)
 same => n,Playback(beep)
 same => n,Goto(svi-ia,3000,1)
 same => n,Hangup()


; ============================================================
; Contexte SVI ESMT
; ============================================================
[ivr-esmt]

; ---- Entrée principale ------------------------------------
exten => s,1,Answer()
 same => n,Set(ATTEMPT=0)
 same => n,Goto(menu,1)

; ---- Menu -------------------------------------------------
exten => menu,1,NoOp(Tentative ${ATTEMPT}/3)
 same => n,Set(ATTEMPT=$[${ATTEMPT} + 1])
 same => n,GotoIf($[${ATTEMPT} > 3]?bye,1)
 same => n,AGI(googletts.agi,"Assalamou anleykum Nagadef Bienvenue à l'ESMT.",fr,any)
 same => n,GotoIf($[${ATTEMPT} > 1]?options,1)
 same => n,AGI(googletts.agi,"Vous êtes dans le serveur vocal interactif.",fr,any)
 same => n(options),AGI(googletts.agi,"Pour joindre Bao, appuyez sur 1.",fr,any)
 same => n,AGI(googletts.agi,"Pour joindre Fatou, appuyez sur 2.",fr,any)
 same => n,AGI(googletts.agi,"Pour réécouter, appuyez sur dièse.",fr,any)
 same => n,Set(TIMEOUT(response)=10)
 same => n,WaitExten(10)

; ---- Choix 1 : Bao ----------------------------------------
exten => 1,1,AGI(googletts.agi,"Transfert vers Bao.",fr,any)
 same => n,Set(CALLED_NUM=1000)
 same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-1000)
 same => n,MixMonitor(${RECFILE}.wav,b)
 same => n,Dial(PJSIP/1000,30,tT)
 same => n,GotoIf($["${DIALSTATUS}" = "NOANSWER"]?vmail-bao,1)
 same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy,1)
 same => n,Hangup()

; ---- Choix 2 : Fatou --------------------------------------
exten => 2,1,AGI(googletts.agi,"Transfert vers Fatou.",fr,any)
 same => n,Set(CALLED_NUM=1001)
 same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-1001)
 same => n,MixMonitor(${RECFILE}.wav,b)
 same => n,Dial(PJSIP/1001,30,tT)
 same => n,GotoIf($["${DIALSTATUS}" = "NOANSWER"]?vmail-fatou,1)
 same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy,1)
 same => n,Hangup()

; ---- Rejouer ----------------------------------------------
exten => #,1,Set(ATTEMPT=0)
 same => n,Goto(menu,1)

exten => _[3-9],1,Goto(menu,1)

; ---- Timeout ----------------------------------------------
exten => t,1,Goto(menu,1)

; ---- Touche invalide --------------------------------------
exten => i,1,AGI(googletts.agi,"Choix invalide.",fr,any)
 same => n,Goto(menu,1)

; ---- Occupé -----------------------------------------------
exten => busy,1,AGI(googletts.agi,"La ligne est occupée. Veuillez rappeler ultérieurement.",fr,any)
 same => n,Hangup()

; ---- Messagerie Bao ---------------------------------------
exten => vmail-bao,1,AGI(googletts.agi,"Bao est absent. Laissez votre message après le signal.",fr,any)
 same => n,VoiceMail(1000@default,u)
 same => n,Hangup()

; ---- Messagerie Fatou -------------------------------------
exten => vmail-fatou,1,AGI(googletts.agi,"Fatou est absente. Laissez votre message après le signal.",fr,any)
 same => n,VoiceMail(1001@default,u)
 same => n,Hangup()

; ---- Au revoir --------------------------------------------
exten => bye,1,AGI(googletts.agi,"Nous n'avons pas reçu votre choix. Au revoir.",fr,any)
 same => n,Hangup()

; [REC] Extension h pour ivr-esmt
exten => h,1,NoOp(Raccrochage ivr-esmt)
 same => n,GotoIf($["${RECFILE}" = ""]?end)
 same => n,Set(REC_BILLSEC=${CDR(billsec)})
 same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|ivr-esmt" >> /tmp/rec_queue.txt)
 same => n(end),NoOp(Fin h ivr-esmt)


; ============================================================
; GoIP — appels entrants
; ============================================================
[from-goip]
exten => _X.,1,NoOp(Appel entrant GoIP depuis ${CALLERID(num)})
 same => n,Goto(ivr-esmt,s,1)
 same => n,Hangup()


; ============================================================
; Messages SIP internes
; ============================================================
[message]
exten => _10XX,1,NoOp(==> Message reçu de ${MESSAGE(from)} pour ${MESSAGE(to)})
 same => n,Set(TO=${EXTEN})
 same => n,Set(FROM=${MESSAGE(from)})
 same => n,Set(MSG=${MESSAGE(body)})
 same => n,Set(MESSAGE(body)=${MSG})
 same => n,MessageSend(pjsip:${TO},${FROM})
 same => n,NoOp(==> Message envoyé à ${TO})
 same => n,Hangup()

exten => 1441,1,NoOp(==> Message reçu de ${MESSAGE(from)})
 same => n,Set(FROM=${MESSAGE(from)})
 same => n,Set(MSG=${MESSAGE(body)})
 same => n,Set(EXT_RAW=${CUT(FROM,<,2)})
 same => n,Set(EXT_RAW=${CUT(EXT_RAW,@,1)})
 same => n,Set(EXT=${CUT(EXT_RAW,:,2)})
 same => n,NoOp(Numero : ${EXT})
 same => n,GotoIf($["${MSG}" = "note"]?call_api,1)
 same => n,Set(MESSAGE(body)=Commande inconnue. Envoyez "note" à 1441.)
 same => n,MessageSend(pjsip:${EXT},pjsip:1441)
 same => n,Hangup()

exten => call_api,1,System(/usr/bin/python3 /var/lib/asterisk/note/get_note.py ${EXT})
 same => n,Set(NOTE=${FILE(/var/lib/asterisk/note/note_${EXT}.txt)})
 same => n,NoOp(==> Contenu du fichier : ${NOTE})
 same => n,GotoIf($["${NOTE}" = ""]?file_not_found)
 same => n,Set(MESSAGE(body)=Votre note est : ${NOTE})
 same => n,MessageSend(pjsip:${EXT},pjsip:1441)
 same => n,Hangup()

exten => file_not_found,1,Set(MESSAGE(body)=Note non trouvée pour l'extension ${EXT})
 same => n,MessageSend(pjsip:${EXT},pjsip:1441)
 same => n,Hangup()


; ============================================================
; Dongle GSM — SMS et appels entrants (inchangé)
; ============================================================
[from-dongle]
exten => sms,1,NoOp(SMS reçu de ${CALLERID(num)})
 same => n,Set(MSG=${BASE64_DECODE(${SMS_BASE64})})
 same => n,Set(NUM=${CALLERID(num)})
 same => n,NoOp(Message décodé : ${MSG})
 same => n,System(echo "${NUM}|${MSG}" >> /tmp/sms_queue.txt)
 same => n,Hangup()

exten => report,1,NoOp(Rapport SMS reçu)
 same => n,Hangup()

exten => s,1,NoOp(Appel entrant GSM depuis ${CALLERID(num)})
 same => n,Goto(ivr-esmt,s,1)

exten => _X.,1,Goto(ivr-esmt,s,1)


3.1 — Principe : 3 ajouts par contexte


Pour chaque extension qui fait un appel, on ajoute ces 2 lignes avant Dial() :

Ajout avant chaque Dial()
asterisk
; 1. Sauvegarder le numéro appelé
 same => n,Set(CALLED_NUM=${EXTEN})
; 2. Définir le nom du fichier d'enregistrement
 same => n,Set(RECFILE=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
; 3. Démarrer l'enregistrement (b = both legs)
 same => n,MixMonitor(${RECFILE}.wav,b)
3.2 — Extension h (déclenchée au raccrochage)
L'extension h s'exécute automatiquement à la fin de chaque appel. Elle écrit dans la file d'attente :

Extension h — à placer dans chaque contexte
asterisk
; Extension h pour from-internal
exten => h,1,NoOp(Raccrochage from-internal)
 same => n,GotoIf($["${RECFILE}" = ""]?end)
 same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-internal" >> /tmp/rec_queue.txt)
 same => n(end),NoOp(Fin)

; Extension h pour from-trunk-b
exten => h,1,NoOp(Raccrochage trunk-b)
 same => n,GotoIf($["${RECFILE}" = ""]?end)
 same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|from-trunk-b" >> /tmp/rec_queue.txt)
 same => n(end),NoOp(Fin)

; Extension h pour ivr-esmt
exten => h,1,NoOp(Raccrochage ivr-esmt)
 same => n,GotoIf($["${RECFILE}" = ""]?end)
 same => n,System(echo "${RECFILE}.wav|${CALLERID(num)}|${CALLED_NUM}|${CDR(billsec)}|${CDR(uniqueid)}|ivr-esmt" >> /tmp/rec_queue.txt)
 same => n(end),NoOp(Fin)
3.3 — Recharger le dialplan

 

Terminal
bash
asterisk -rx "dialplan reload"

 

# Vérifier que MixMonitor est bien dans le dialplan
asterisk -rx "dialplan show from-internal" | grep MixMonitor
# Doit afficher 3 lignes contenant MixMonitor

# Vérifier que h est présent
asterisk -rx "dialplan show from-internal" | grep -A4 "exten => h"

Pourquoi RECFILE et pas MIXMONITOR_FILENAME ?
La variable MIXMONITOR_FILENAME est vide dans l'extension h car elle appartient à un autre thread Asterisk. On utilise notre propre variable RECFILE définie sur le canal.
 

File d'attente et traitement automatique via cron


Pourquoi une file d'attente ? MixMonitor ferme le fichier WAV après l'extension h. On ne peut donc pas convertir immédiatement. La solution : écrire dans une file et traiter 1 minute plus tard avec cron.

4.1 — Script de traitement (process_queue.sh)

/var/lib/asterisk/recording/process_queue.sh


bash
#!/bin/bash
QUEUE="/tmp/rec_queue.txt"
LOCK="/tmp/rec_queue.lock"
SCRIPT="/usr/bin/python3 /var/lib/asterisk/recording/post_record.py"

# Éviter les exécutions simultanées
[ -f "$LOCK" ] && exit 0
touch "$LOCK"

[ ! -f "$QUEUE" ] && rm -f "$LOCK" && exit 0
[ ! -s "$QUEUE" ] && rm -f "$LOCK" && exit 0

# Copier et vider la file atomiquement
cp "$QUEUE" "${QUEUE}.processing"
> "$QUEUE"

# Traiter chaque ligne : WAV|CALLER|CALLEE|BILLSEC|UNIQUEID|CONTEXT
while IFS='|' read -r WAV CALLER CALLEE BILLSEC UNIQUEID CONTEXT; do
    [ -z "$WAV" ] && continue
    [ ! -f "$WAV" ] && continue
    $SCRIPT "$WAV" "$CALLER" "$CALLEE" "$BILLSEC" "$UNIQUEID" "$CONTEXT"
done < "${QUEUE}.processing"

rm -f "${QUEUE}.processing" "$LOCK"

4.2 — Configuration du cron

Terminal
bash
chmod +x /var/lib/asterisk/recording/process_queue.sh
chown asterisk:asterisk /var/lib/asterisk/recording/process_queue.sh

# Ajouter au crontab root
crontab -e
Ajoutez cette ligne dans le crontab :
bash
# Traiter les enregistrements toutes les minutes
* * * * * /var/lib/asterisk/recording/process_queue.sh



Interface Web PHP

L'interface se compose de 3 fichiers PHP dans /var/www/html/recordings/ :

5.1 — config.php (configuration + auth)

/var/www/html/recordings/config.php


php
<?php
define('DB_HOST',  'localhost');
define('DB_USER',  'asterisk_rec');
define('DB_PASS',  'Esmt@2025!');
define('DB_NAME',  'asterisk_recordings');
define('WEB_USER', 'admin');
define('WEB_PASS', 'esmt2025');
define('AUDIO_DIR','/var/spool/asterisk/monitor');
define('PER_PAGE', 20);

function get_pdo(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $dsn = "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=utf8mb4";
        $pdo = new PDO($dsn, DB_USER, DB_PASS, [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    }
    return $pdo;
}

function require_auth(): void {
    $user = $_SERVER['PHP_AUTH_USER'] ?? '';
    $pass = $_SERVER['PHP_AUTH_PW']   ?? '';
    if ($user !== WEB_USER || $pass !== WEB_PASS) {
        header('WWW-Authenticate: Basic realm="ESMT Recordings"');
        header('HTTP/1.0 401 Unauthorized');
        exit;
    }
}

5.2 — audio.php (streaming sécurisé)


Ce fichier sert les MP3 avec support Range (lecture partielle HTML5) et vérifie l'authentification avant chaque accès.

Fonctionnement de audio.php
text
1. require_auth()              → vérifie login/password
2. basename($_GET['f'])        → sécurité anti path-traversal
3. preg_match('/\.mp3$/i')     → accepte uniquement les MP3
4. strpos(realpath($path), ...) → double vérification du chemin
5. HTTP Range support          → permet seek dans l'audio HTML5
5.3 — Permissions des fichiers Web
Terminal
bash
chown -R www-data:www-data /var/www/html/recordings/
chmod 644 /var/www/html/recordings/*.php

# Vérifier que PHP-MySQL est installé
php -m | grep -i pdo
# Doit afficher : PDO et pdo_mysql

# Si absent :
apt-get install php-mysql -y
systemctl restart apache2
 

Tests et validation


Test 1 — Vérifier la chaîne complète
Terminal — surveillance
bash
# Terminal 1 : surveiller les fichiers
watch -n1 "ls -lh /var/spool/asterisk/monitor/ | tail -5"

# Terminal 2 : surveiller les logs
tail -f /var/log/asterisk/recording_errors.log

# Terminal 3 : surveiller la file
watch -n1 "cat /tmp/rec_queue.txt 2>/dev/null || echo 'File vide'"
Appelez 1000 → 1001 depuis Linphone/Zoiper, parlez 5 secondes, raccrochez.

Séquence attendue
Log Asterisk console (asterisk -rvvv)
text
-- MixMonitor Recording PJSIP/1000-XXXXXXXX        ← enregistrement démarré
-- PJSIP/1001 answered                              ← appel décroché
-- Channel left 'simple_bridge'                     ← appel terminé
-- Executing [h@from-internal:1] NoOp(...)          ← extension h lancée
-- Executing [h@from-internal:2] GotoIf(...)        ← RECFILE non vide → continue
-- Executing [h@from-internal:3] System(echo ...)   ← écriture dans la file
== End MixMonitor Recording                         ← WAV fermé
Après < 1 minute — log post_record.py
text
[INFO] WAV converti → /var/spool/asterisk/monitor/20260423-XXXXXX-1000-1001.mp3
[INFO] OK | caller=1000 callee=1001 dur=5s uid=...
Test 2 — Vérifier MySQL
Terminal
bash
mysql -u asterisk_rec -p'Esmt@2025!' asterisk_recordings \
  -e "SELECT id, calldate, caller, callee, duration, status
       FROM recordings ORDER BY id DESC LIMIT 5;"

URL      : http://VOTREIP/recordings/
Login    : admin
Password : esmt2025

 



Le lecteur audio HTML5 utilise audio.php?f=fichier.mp3 qui vérifie l'authentification à chaque accès. Les fichiers MP3 ne sont jamais exposés directement.

 

 


Dépannage rapide

Problème Cause probable  Solution
WAV reste, pas de MP3  lame absent    apt-get install lame -y
File vide après appel  RECFILE vide ou h absent Vérifier dialplan show from-internal
MySQL vide  mysql-connector absent pip3 install mysql-connector-python
Audio ne joue pas www-data sans accès monitor usermod -aG asterisk www-data
Page Web : accès refusé config.php absent  Vérifier ls /var/www/html/recordings/

    
Résultat final

À la fin du TP, le système complet est opérationnel :

  • Chaque appel SIP est automatiquement enregistré en WAV par MixMonitor
  • Au raccrochage, l'extension h écrit dans /tmp/rec_queue.txt
  • Le cron (toutes les minutes) lance process_queue.sh
  • Python convertit WAV→MP3 avec lame et insère dans MySQL
  • L'interface Web affiche l'historique avec filtres (numéro, date, contexte)
  • Chaque enregistrement est écoutable directement dans le navigateur
  • L'accès est protégé par authentification HTTP Basic
  • Les fichiers audio ne sont jamais exposés directement (proxy PHP)

RÉSUMÉ DES COMPOSANTS

Composant Technologie  Rôle
MixMonitor  Asterisk   Capture audio des deux côtés de l'appel
Extension h Asterisk dialplan Déclenchée au raccrochage → écrit dans la file
process_queue.sh Bash + cron Lit la file toutes les minutes
post_record.py  Python 3  Convertit WAV→MP3, insère dans MySQL
recordings (table)  MySQL 8 Stocke toutes les métadonnées
index.php  PHP 8.3 Interface Web : historique + filtres
audio.php   PHP 8.3  Streaming MP3 sécurisé (HTTP Range)

     
    

🎬 Démo vidéo Démonstration complète du système en action 📞 Appel SIP entre 1000 et 1001 — enregistrement automatique et consultation via l'interface Web

Pour être informé des derniers articles, inscrivez vous :
Commentaires