3.3.1. 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.

3.3.1.1. Abhängigkeiten

Dependencies lassen sich der folgenden Cargo.toml Datei entnehmen:

Codeabschnitt 3.3.1 Cargo.toml
 1[package]
 2name = "rfid-login-client"
 3version = "0.1.0"
 4edition = "2021"
 5
 6# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 7
 8[dependencies]
 9anyhow = { version = "1.0.70" }
10axum = { version = "0.6.9", features = ["macros", "ws"] }
11serde = { version = "1.0.159" }
12serde_json = { version = "1.0.93" }
13serde_with = { version = "2.3.1" }
14sqlite = "0.30.4"
15tokio = { version= "1.27.0", features=["full"]}
16tokio-util = { version = "0.7.7", features = ["full"] }
17tower-http = { version = "0.3.4", features = ["cors"] }
18tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
19
20# Add RFID dependencies only to linux machines
21[target.'cfg(target_os = "linux")'.dependencies]
22embedded-hal = { version = "0.2.7" }
23linux-embedded-hal = { version = "0.3.2" }
24mfrc522 = { 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.

3.3.1.2. RFID

Zur Verwendung meines RFID-RC522 Readers habe ich die mfrc522 crate hinzugefügt.

Der RFID-RC522 Reader kommunitiert mit dem Pi über SPI. Die linux-embedded-hal stellt ein SPI Interface zur Verfügung:

Codeabschnitt 3.3.2 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:

Codeabschnitt 3.3.3 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<u8>) zurückgegeben:

Codeabschnitt 3.3.4 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<u8> 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.

3.3.1.3. Webserver

Wie eben erwähnt, wurde als Webserver wieder Axum (wie in Projekt 1) 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:

Codeabschnitt 3.3.5 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<Snapshot>,
}

#[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<AppState>,
) -> 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<Snapshot>) -> 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<Snapshot>) -> Router liefert einen axum::Router zurück, welcher einen tokio::sync::broadcast::Sender<Snapshot> 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:

Codeabschnitt 3.3.6 bin/rfid-login-client.rs
#[tokio::main]
async fn main() {
    let (rfid_tx, _) = broadcast::channel::<rfid_login_client::routing::Snapshot>(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<AppState> übergeben werden kann.

3.3.1.4. 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.

Codeabschnitt 3.3.7 persistence.rs
#[serde_as]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct User {
    pub name: String,
    pub checksum: String,
    pub rfid_uid: Option<u32>,
}

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<u8>) -> Result<u32, String> in einen u32 Typen umgewandelt, sodass die UID gespeichert werden kann:

Codeabschnitt 3.3.8 persistence.rs
16pub fn as_u32_be(vector: Vec<u8>) -> Result<u32, String> {
17    if vector.len() != 4 {
18        Err(String::from("Vector must contain exactly 4 elements!"))
19    } else {
20        Ok(((vector[0] as u32) << 24) +
21        ((vector[1] as u32) << 16) +
22        ((vector[2] as u32) <<  8) +
23        ((vector[3] as u32) <<  0))
24    }
25}

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.

Codeabschnitt 3.3.9 persistence.rs
81// --snip--
82let connection = sqlite::open("./sqlite.db").unwrap();
83let query = format!("
84    SELECT * FROM users
85    WHERE rfid_uid={uid_u32};
86");
87let mut statement = connection.prepare(&query);
88// Try to prepare the statement, until the lock is free
89while statement.is_err() {
90    statement = connection.prepare(&query);
91}
92// --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.