Skip to main content

Crate kdeconnect_proto

Crate kdeconnect_proto 

Source
Expand description

kdeconnect-proto is a pure Rust modular implementation of the KDE Connect protocol.

It implements the transport layer as well as full serializing/deserializing of all packet types according to the specification. It’s up to the user of this library to decide what to do when a packet of a specific type is received.

It’s written using an abstraction over all IO usage, hence it supports a wide range of hardware. A backend using tokio is provided by default as well as a backend for embedded devices using Embassy in the kdeconnect-embassy crate.

The entrypoint of the API is the Device structure which represents the host device KDE Connect client.

This crate has the embedded feature which enables the use of a crypto implementation for embedded devices, used in TLS connections. You should enable it if you develop for embedded devices, don’t use the default features of this crate, or use kdeconnect-embassy.

§Getting started

Add to your Cargo.toml file kdeconnect-proto, uuid (with the feature v4), tokio (with the feature rt-multi-thread) and rcgen (optional, read the documentation of DeviceConfig for more information).

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("Connected")).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;
    }
}

Modules§

config
Define the configuration of the host device.
device
Define structures related to connected devices.
error
Define a custom Result type.
io
Module defining the abstraction over IO usage.
packet
Definition of all the KDE Connect core packets.
plugin
Define the Plugin trait used to make KDE Connect plugins.
transport
Transport-layer implementation of the KDE Connect protocol.
trust
Define the TrustHandler triat which is used to manage trust of devices. Check out its documentation for more details.

Attribute Macros§

async_trait