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
Plugintrait used to make KDE Connect plugins. - transport
- Transport-layer implementation of the KDE Connect protocol.
- trust
- Define the
TrustHandlertriat which is used to manage trust of devices. Check out its documentation for more details.