kamailio-rpc 0.1.1

## Minimal crate to send RPC commands to Kamailio using FIFOs
Documentation
use crate::error::KamailioRpcError;
use crate::error::KamailioRpcError::{KamailioError, NoResponseOrError};
use libc::{mkfifo, mode_t, EACCES, EEXIST, ENOENT};
use log::{trace, warn};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::ffi::{c_int, CString};
use std::fs::Permissions;
use std::io;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use tokio::fs;
use tokio::fs::{set_permissions, OpenOptions};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::sync::oneshot;
use uuid::Uuid;

pub mod error;

#[derive(Serialize, Deserialize, Debug)]
struct JsonRpcRequest {
    #[serde(rename = "jsonrpc")]
    json_rpc: String,
    method: String,
    id: i32,
    reply_name: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct JsonRpcResponse {
    #[serde(rename = "jsonrpc")]
    json_rpc: String,
    result: Option<Value>,
    error: Option<Value>,
    id: i32,
}

pub async fn kamailio_rpc(kamailio_cmd: &str) -> Result<Value, KamailioRpcError> {
    let fifo_id = Uuid::new_v4();
    let name = format!("kamailio_receiver_{}", fifo_id);
    let path = format!("/tmp/{}", name);

    delete_fifo_if_exists(&path).await?;

    create_fifo(&path, Some(0o666))?;
    set_permissions(&path, Permissions::from_mode(0o666)).await?;

    let request = JsonRpcRequest {
        json_rpc: "2.0".to_string(),
        method: kamailio_cmd.to_string(),
        id: 1,
        reply_name: name,
    };

    let request_json = serde_json::to_string(&request)?;

    let response =
        send_and_receive("/run/kamailio/kamailio_rpc.fifo", &path, &request_json).await?;
    let response_json: JsonRpcResponse = serde_json::from_str(&response)?;
    println!("Response received: {:?}", response_json);

    delete_fifo_if_exists(&path).await?;

    if let Some(result) = response_json.result {
        return Ok(result);
    } else if let Some(error) = response_json.error {
        return Err(KamailioError(error));
    }

    Err(NoResponseOrError)
}

async fn send_and_receive(
    send_fifo: &str,
    receive_fifo: &str,
    request: &str,
) -> Result<String, KamailioRpcError> {
    let (tx, rx) = oneshot::channel();

    // listen for response
    let receive_fifo = receive_fifo.to_string();
    tokio::spawn(async move {
        match OpenOptions::new().read(true).open(&receive_fifo).await {
            Ok(file) => {
                let mut reader = BufReader::new(file);
                let mut response = String::new();

                loop {
                    match reader.read_line(&mut response).await {
                        Ok(0) => break,
                        Ok(_) => {}
                        Err(e) => warn!("Error reading from FIFO socket: {}", e),
                    }
                }

                if let Err(e) = tx.send(response) {
                    warn!("Failed to send received Kamailio response: {}", e);
                };
            }
            Err(e) => {
                warn!("Could not open the receiving FIFO {}: {}", receive_fifo, e);
                return;
            }
        }
    });

    trace!("Sending request: {}", request);

    fs::write(send_fifo, request).await?;
    trace!("Request sent");

    Ok(rx.await?)
}

async fn delete_fifo_if_exists(path: &String) -> Result<(), KamailioRpcError> {
    match fs::remove_file(&path).await {
        Ok(_) => trace!("Existing FIFO removed"),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            trace!("No need to delete existing FIFO")
        }
        Err(e) => {
            return Err(e.into());
        }
    };
    Ok(())
}

fn create_fifo<P: AsRef<Path>>(path: P, mode: Option<u32>) -> io::Result<()> {
    let path = CString::new(path.as_ref().to_str().unwrap())?;
    let mode = mode.unwrap_or(0o644);
    let result: c_int = unsafe { mkfifo(path.as_ptr(), mode as mode_t) };

    let result: i32 = result.into();
    if result == 0 {
        return Ok(());
    }

    let error = errno::errno();
    return match error.0 {
        EACCES => Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            format!("could not open {:?}: {}", path, error),
        )),
        EEXIST => Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!("could not open {:?}: {}", path, error),
        )),
        ENOENT => Err(io::Error::new(
            io::ErrorKind::NotFound,
            format!("could not open {:?}: {}", path, error),
        )),
        _ => Err(io::Error::new(
            io::ErrorKind::Other,
            format!("could not open {:?}: {}", path, error),
        )),
    };
}