# Rust Backend Das Backend des Webclients ist in Rust implementiert. Rust war meine Wahl, da ich bereits Erfahrung mit Webservern und Websockets sammeln konnte, und mich primär auf RFID konzentrieren möchte. ## Abhängigkeiten Dependencies lassen sich der folgenden *Cargo.toml* Datei entnehmen: ```{code-block} toml --- lineno-start: 1 caption: Cargo.toml --- [package] name = "rfid-login-client" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = { version = "1.0.70" } axum = { version = "0.6.9", features = ["macros", "ws"] } serde = { version = "1.0.159" } serde_json = { version = "1.0.93" } serde_with = { version = "2.3.1" } sqlite = "0.30.4" tokio = { version= "1.27.0", features=["full"]} tokio-util = { version = "0.7.7", features = ["full"] } tower-http = { version = "0.3.4", features = ["cors"] } tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } # Add RFID dependencies only to linux machines [target.'cfg(target_os = "linux")'.dependencies] embedded-hal = { version = "0.2.7" } linux-embedded-hal = { version = "0.3.2" } mfrc522 = { version = "0.5.0" } ``` Im unteren Abschnitt wird hierbei darauf geachtet, dass gewisse dependencies nur auf Linux Geräten gebraucht werden, da die *Hardware Abstraction Layers* (hal) auf anderen Betriebssystemen nicht funktionieren. Der Client ist darauf ausgelegt, dass man alle Funktionen, bis auf das RFID, unabhängig vom Betriebssystem verwenden kann. ## RFID Zur Verwendung meines *RFID-RC522* Readers habe ich die [mfrc522 crate](https://gitlab.com/jspngh/rfid-rs) hinzugefügt. Der *RFID-RC522* Reader kommunitiert mit dem Pi über *SPI*. Die *linux-embedded-hal* stellt ein *SPI* Interface zur Verfügung: ```{code-block} rust --- caption: rfid.rs --- use linux_embedded_hal as hal; use hal::{ spidev::{SpiModeFlags, SpidevOptions}, Spidev }; // --snip-- let mut spi = Spidev::open("/dev/spidev0.0").unwrap(); let options = SpidevOptions::new() .max_speed_hz(1_000_000) .mode(SpiModeFlags::SPI_MODE_0) .build(); spi.configure(&options).unwrap(); ``` Als nächstes wird der Chip Select Pin (22) gesetzt, um dem *RFID-RC522* Reader zu signalisieren, dass Daten empfangen werden können. Anschließend wird eine Instanz des `Mfrc522` Typs erstellt und die Version gechecked: ```{code-block} rust --- caption: rfid.rs --- use embedded_hal::blocking::delay::DelayMs; use hal::{Delay, Pin}; use mfrc522::Mfrc522; let mut delay = Delay; // softwaregesteuerter CS Pin let pin = Pin::new(22); pin.export().unwrap(); while !pin.is_exported() {} // Kurzes Delay, da pin.is_exported() manchmal zu früh returned delay.delay_ms(1u32); pin.set_direction(Direction::Out).unwrap(); pin.set_value(1).unwrap(); let mut mfrc522 = Mfrc522::new(spi) .with_nss(pin) .init() .unwrap(); let vers = mfrc522.version().unwrap(); assert!(vers == 0x91 || vers == 0x92); ``` Nun, da der *RFID-RC522* Reader initialisiert wurde, kann gelesen werden. Zunächst wird ein `Loop` erstellt, in dem immer wieder Typ A Requests gesendet werden. Kommt eine Antwort auf diese Anfrage zurück, wird die Antwort weiter verarbeitet. Über die `mfrc522` crate wird dann die UID der *Proximity Integrated Circuit Card* (**PICC**) ausgelesen und anschließend als *Byte-Vector* (`Vec`) zurückgegeben: ```{code-block} rust --- caption: rfid.rs --- loop { // Sende Typ A Request an PICCs in der Nähe if let Ok(atqa) = mfrc522.reqa() { // Falls eine Antwort erhalten wird, extrahiere die UID der PICC if let Ok(uid) = mfrc522.select(&atqa) { // Gebe die UID als Vec zurück. Erfolg! return Ok(uid.as_bytes().to_vec()) } // Kein Error, stattdessen einfach neuer Versuch. } // kurzes Delay, bevor die nächste Typ A Request gesendet wird delay.delay_ms(1000u32); } ``` Die UID der PICC kann dann zum Authentifizieren eingesetzt werden. ## Webserver Wie eben erwähnt, wurde als Webserver wieder Axum (wie in [Projekt 1](https://gitlab.informatik.hs-augsburg.de/dva/berichte-2023/56/-/tree/main/projects/webserver)) verwendet. Die einfache Implementierung von `Extractor` und `Websocket` macht Axum in diesem Bereich zu meinem Favoriten. Der Websocket sendet immer dann `Snapshots` an den Endpunkt */auth/rfid*, wenn eine `RFID UID` aus der eben genannten Funktion erfasst wird. Diese `RFID UID` wird dann gegen eine Datenbank mit Nutzern abgeglichen. Falls ein Nutzer bereits eine Karte zu seinem Account hinzugefügt hat, kann dieser sich authentifizieren, ohne sein Passwort eintippen zu müssen. Das Frontend kann am Endpunkt *ws://{IP_ADDRESS}:8080/auth/rfid* den Websocket subscriben und Callbacks zu diesen Events hinzufügen. Das Callback, das dadruch aufgerufen wird, muss dann anhand des States festlegen, was unternommen wird. Der Endpunkt wird wie folgt definiert: ```{code-block} rust --- caption: routing.rs --- use axum::{ extract::{ ws::{Message, WebSocket}, State, WebSocketUpgrade, }, routing::post, Router, }; use serde_with::serde_as; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; #[derive(Clone)] struct AppState { rfid_tx: broadcast::Sender, } #[serde_as] #[derive(Clone, Serialize, Deserialize, Debug)] pub enum Snapshot { RfidUser(crate::persistence::User), RfidUID(u32), RfidERR(String), } #[axum::debug_handler] async fn rfid_get( ws: WebSocketUpgrade, State(state): State, ) -> impl IntoResponse { ws.on_upgrade(|ws: WebSocket| async { rfid_stream(state, ws).await }) } async fn rfid_stream(app_state: AppState, mut ws: WebSocket) { let mut rx = app_state.rfid_tx.subscribe(); while let Ok(msg) = rx.recv().await { ws.send(Message::Text(serde_json::to_string(&msg).unwrap())) .await .unwrap(); } } pub fn get_router(rfid_tx: broadcast::Sender) -> Router { // --snip-- let app_state = AppState { rfid_tx }; Router::new() // --snip-- .route("/auth/rfid", get(rfid_get)) .with_state(app_state) // --snip-- } ``` `fn get_router(rfid_tx: broadcast::Sender) -> Router` liefert einen `axum::Router` zurück, welcher einen `tokio::sync::broadcast::Sender` als shared State verwendet. `app_state.rfid_tx` is ein `Channel` aus `tokio::sync::broadcast`, mit welchem Informationen zwischen Threads ausgetauscht werden können. In der `main` Funktion wird ein Thread erstellt, der die ganze zeit läuft und RFID UIDs anfrägt. Sobald eine ankommt, wird sie als `Snapshot` verpackt und an den Broadcast Channel gesendet: ```{code-block} rust --- caption: bin/rfid-login-client.rs --- #[tokio::main] async fn main() { let (rfid_tx, _) = broadcast::channel::(1); let router = rfid_login_client::routing::get_router(rfid_tx.clone()); tokio::task::spawn_blocking(move || { loop { match rfid_login_client::rfid::read_card() { Ok(uid) => { if let Some(user) = rfid_login_client::persistence::get_user_from_rfid_uid(uid.clone()) { println!("Got User: {}", user.name); let _ = rfid_tx.send(Snapshot::RfidUser(user)); } else { println!("Unpacking UID ..."); match rfid_login_client::persistence::as_u32_be(uid.clone()) { Ok(rfid_uif) => { println!("Serving UID"); let _ = rfid_tx.send(Snapshot::RfidUID(rfid_uif)); }, Err(err) => { println!("Got error: {err}"); let _ = rfid_tx.send(Snapshot::RfidERR(err)); } }; } }, Err(err) => { println!("Got error: {err:?}"); let _ = rfid_tx.send(Snapshot::RfidERR(err)); } }; // Nicht-Linux Systeme breaken aus dem Loop, da die RFID nicht unterstützt ist #[cfg(not(target_os = "linux"))] break; } }); // --snip-- // Create router, etc. } ``` `rfid_tx` ist dabei der Sender und `rx` der Empfänger. `rx` wurde im vorherigen Beispiel über `app_state.rfid_tx.subscribe()` erzeugt. Dies ist möglich, da die Implementierung von `axum::extract::State` allen Endpunkten über `State(state): State` übergeben werden kann. ## Persistierung Die Persisiterung der Daten erfolgt über eine simple `sqlite` Datenbank. In dieser existiert eine Tabelle namens "users", welche Informationen über Passwörter, RFID UIDs und den Namen der Nutzer hält. ```{code-block} rust --- caption: persistence.rs --- #[serde_as] #[derive(Clone, Serialize, Deserialize, Debug)] pub struct User { pub name: String, pub checksum: String, pub rfid_uid: Option, } pub fn init_sqlitedb() { let connection = sqlite::open("./sqlite.db").unwrap(); let query = " CREATE TABLE IF NOT EXISTS users ( name TEXT PRIMARY KEY, checksum TEXT NOT NULL, rfid_uid NUMBER ); "; connection.execute(query).unwrap(); } ``` Die RFID UID wird nach *big endian* Interpretation von der Funktion `as_u32_be(vector: Vec) -> Result` in einen u32 Typen umgewandelt, sodass die UID gespeichert werden kann: ```{code-block} rust --- lineno-start: 16 caption: persistence.rs --- pub fn as_u32_be(vector: Vec) -> Result { if vector.len() != 4 { Err(String::from("Vector must contain exactly 4 elements!")) } else { Ok(((vector[0] as u32) << 24) + ((vector[1] as u32) << 16) + ((vector[2] as u32) << 8) + ((vector[3] as u32) << 0)) } } ``` Beim Schreiben und lesen der Datenbank werden auf `Locks` gewartet, da mehrere Threads gleichzeitig auf die Datenbank zugreifen könnten. Nur beim Erstellen in `init_sqlitedb()` geht das Programm davon aus, dass es noch kein Lock gibt, da es der erste Befehl ist, den das Programm ausführt. ```{code-block} rust --- lineno-start: 81 caption: persistence.rs --- // --snip-- let connection = sqlite::open("./sqlite.db").unwrap(); let query = format!(" SELECT * FROM users WHERE rfid_uid={uid_u32}; "); let mut statement = connection.prepare(&query); // Try to prepare the statement, until the lock is free while statement.is_err() { statement = connection.prepare(&query); } // --snip-- ``` Es wurden noch weitere Funktionen angelegt, um die Datenbank zu verwalten, welche aber im RFID Kontext unwichtig sind und aus dem Sourcecode entnommen werden können. Diese Funktionen umfassen das Anlegen neuer Nutzer, das Updaten von Nutzern und die Abfrage von Nutzerdaten. Der Sourcecode des Backends befindet sich im [Gitlab](https://gitlab.informatik.hs-augsburg.de/dva/berichte-2023/56/-/tree/main/projects/rfid/rust-login-client).