kdeconnect-proto 0.1.2

A pure Rust modular implementation of the KDE Connect protocol
Documentation
use std::{fs, collections::HashMap, path::{Path, PathBuf}, sync::Arc};
use kdeconnect_proto::{
    config::DeviceConfig,
    device::{Device, DeviceType, Link},
    trust::TrustHandler,
    io::TokioIoImpl,
    packet::{NetworkPacket, NetworkPacketBody, NetworkPacketType},
    plugin::Plugin,
};
use rcgen::{CertificateParams, DistinguishedName, DnType, IsCa, KeyPair};

// 1. Generate a self-signed certificate and a private key using rcgen

pub fn gen_certificate() {
    let device_id = uuid::Uuid::new_v4().to_string().replace('-', "");
    let mut params = CertificateParams::default();
    params.is_ca = IsCa::ExplicitNoCa;

    let mut dn = DistinguishedName::new();
    dn.push(DnType::CommonName, device_id);
    dn.push(DnType::OrganizationName, "KDE");
    dn.push(DnType::OrganizationalUnitName, "KDE Connect");
    params.distinguished_name = dn;

    let key_pair = KeyPair::generate().unwrap();
    let cert = params.self_signed(&key_pair).unwrap();

    fs::write("private_key.pem", key_pair.serialize_pem()).unwrap();
    fs::write("cert.pem", cert.pem()).unwrap();
}

// 2. Manage device trust by making a structure implementing the TrustHandler trait

struct TrustHandlerImpl {
   path: PathBuf,
   trusted_devices: HashMap<String, Vec<u8>>,
}

impl TrustHandlerImpl {
   pub fn new<P: AsRef<Path>>(path: P) -> Self {
       let path = path.as_ref();

       let trusted_devices = if path.exists() {
            HashMap::from_iter(fs::read_dir(path).unwrap().filter_map(Result::ok).map(|f| {
                let device_id = f.path().file_stem().unwrap().to_string_lossy().to_string();
                let cert = fs::read(f.path()).expect("failed to read certificate");
                (device_id, cert)
            }))
        } else {
            fs::create_dir_all(path).expect("failed to create directory for trusted devices");
            HashMap::new()
        };

       Self {
           path: path.to_path_buf(),
           trusted_devices,
       }
   }
}

#[kdeconnect_proto::async_trait]
impl TrustHandler for TrustHandlerImpl {
    async fn trust_device(&mut self, device_id: String, cert: Vec<u8>) {
       fs::write(self.path.join(device_id.clone() + ".pem"), &cert).unwrap();
       self.trusted_devices.insert(device_id, cert);
   }

   async fn untrust_device(&mut self, device_id: &str) {
       fs::remove_file(self.path.join(device_id.to_string() + ".pem")).unwrap();
       self.trusted_devices.remove(device_id);
   }

   async fn get_certificate(&mut self, device_id: &str) -> Option<&[u8]> {
       self.trusted_devices.get(device_id).map(|v| &**v)
   }
}

// 3. Make as many plugins as you need by making structures implementing the Plugin trait

struct PingPlugin;

#[kdeconnect_proto::async_trait]
impl Plugin for PingPlugin {
    fn supported_incoming_packets(&self) -> Vec<NetworkPacketType> {
        vec![NetworkPacketType::Ping]
    }

    fn supported_outgoing_packets(&self) -> Vec<NetworkPacketType> {
        vec![NetworkPacketType::Ping]
    }

    async fn on_packet_received(
        &self,
        packet: &NetworkPacket,
        link: &Link,
    ) -> kdeconnect_proto::error::Result<()> {
        match &packet.body {
            NetworkPacketBody::Ping(packet) => {
                let device_name = link
                    .info
                    .device_name
                    .as_ref()
                    .unwrap_or(&link.info.device_id);
                let msg = if let Some(msg) = &packet.message {
                    format!(" \"{msg}\"")
                } else {
                    String::new()
                };
                println!("PING{msg} from {device_name}!",);
            }
            _ => unreachable!(),
        }
        Ok(())
    }

    async fn on_start(&self, link: &Link) -> kdeconnect_proto::error::Result<()> {
        link.send(NetworkPacket::ping("It works!")).await;
        Ok(())
    }
}

#[tokio::main]
async fn main() {
    if !Path::new("cert.pem").exists() {
        gen_certificate();
    }

    // 4. Make a device configuration for the host using the DeviceConfig structure

    let config = DeviceConfig {
        name: String::from("kdeconnect client"),
        device_type: DeviceType::Desktop,
        cert: fs::read("cert.pem").expect("failed to read certificate"),
        private_key: fs::read("private_key.pem").expect("failed to read private key"),
    };

    // 5. Start discovering other devices by calling Device::start or Device::start_arced

    let device = Device::new(
        config,
        vec![Box::new(PingPlugin)],
        TrustHandlerImpl::new("trusted_devices"),
        TokioIoImpl,
    );
    let device = Arc::new(device);
    {
        let device = Arc::clone(&device);
        std::thread::spawn(move || {
            device.start_arced();
        });
    }

    // 6. When another device connects, pair with it immediately by calling Device::pair_with

    println!("Started discovering devices");
    loop {
        let link_id = device.wait_for_connection().await;
        println!("Device {link_id} found");
        device.pair_with(&link_id).await;
    }
}