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:
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:
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:
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:
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:
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:
#[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.
#[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:
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.
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.