main.rs (7594B)
1 use pcsc::{Context, Protocols, Scope, ShareMode}; 2 use shared::{MemberDirectory, UserId}; 3 use std::thread::sleep; 4 use std::time::Duration; 5 6 struct NfcSender { 7 core_url: String, 8 token: String, 9 member_directory: MemberDirectory, 10 } 11 12 impl NfcSender { 13 fn new() -> Self { 14 Self { 15 core_url: String::new(), 16 token: String::new(), 17 member_directory: MemberDirectory::new(vec![]), 18 } 19 } 20 fn init(&mut self) -> Result<(), Box<dyn std::error::Error>> { 21 self.core_url = 22 std::env::var("CORE_URL").unwrap_or_else(|_| "https://localhost:8443".to_string()); 23 self.member_directory = serde_json::from_str(&std::env::var("MEMBER_DIRECTORY")?)?; 24 let allowed_devices: serde_json::Value = 25 serde_json::from_str(&std::env::var("ALLOWED_DEVICES")?)?; 26 let secret: &str = allowed_devices["nfc"] 27 .as_str() 28 .ok_or("ALLOWED_DEVICES.nfc is not a string or missing")?; 29 self.login(secret.to_string())?; 30 Ok(()) 31 } 32 33 fn login(&mut self, secret: String) -> Result<(), Box<dyn std::error::Error>> { 34 let login_request = shared::LoginRequest { 35 device_id: "nfc".to_string(), 36 device_secret: secret, 37 }; 38 let response: shared::LoginResponse = 39 ureq::post(&format!("{}/api/v1/login", self.core_url)) 40 .send_json(login_request)? 41 .body_mut() 42 .read_json::<shared::LoginResponse>()?; 43 self.token = response.access_token; 44 Ok(()) 45 } 46 fn update_presence(&self, user_id: &UserId, presence: &str) -> Result<(), ureq::Error> { 47 let request_body = shared::PresenceUpdateRequest { 48 presence: presence.to_string(), 49 status_msg: "NFC toggle request sent".to_string(), 50 }; 51 let _ = ureq::put(&format!( 52 "{}/api/v1/presence/{}/status", 53 self.core_url, user_id.id 54 )) 55 .header("Authorization", &format!("Bearer {}", self.token)) 56 .send_json(request_body)?; 57 Ok(()) 58 } 59 fn get_presence(&self, user_id: &UserId) -> Result<String, ureq::Error> { 60 let now_presence_res = ureq::get(&format!( 61 "{}/api/v1/presence/{}/status", 62 self.core_url, user_id.id 63 )) 64 .header("Authorization", &format!("Bearer {}", self.token)) 65 .call()? 66 .body_mut() 67 .read_json::<shared::PresenceResponse>()?; 68 Ok(now_presence_res.presence) 69 } 70 fn toggled_presence(presence: &str) -> &'static str { 71 if presence == "inLab" { 72 "offline" 73 } else { 74 "inLab" 75 } 76 } 77 } 78 fn main() -> Result<(), Box<dyn std::error::Error>> { 79 let _ = dotenvy::dotenv(); 80 let ctx = Context::establish(Scope::User)?; 81 82 let mut nfc_sender: NfcSender = NfcSender::new(); 83 nfc_sender.init()?; 84 85 println!("NFC sender has started..."); 86 87 loop { 88 let mut readers_buf = [0u8; 2048]; 89 let mut readers = match ctx.list_readers(&mut readers_buf) { 90 Ok(r) => r, 91 Err(_) => { 92 sleep(Duration::from_secs(1)); 93 continue; 94 } 95 }; 96 97 let reader_name = match readers.next() { 98 Some(name) => name, 99 None => { 100 sleep(Duration::from_secs(1)); 101 continue; 102 } 103 }; 104 105 if let Ok(card) = ctx.connect(reader_name, ShareMode::Shared, Protocols::ANY) { 106 let felica_poll_cmd = [ 107 0xFF, 0x00, 0x00, 0x00, 0x08, 0xD4, 0x40, 0x01, 0x00, 0xFF, 0xFF, 0x01, 0x00, 108 ]; 109 let mut rapdu_buf = [0u8; 258]; 110 111 if let Ok(rapdu) = card.transmit(&felica_poll_cmd, &mut rapdu_buf) { 112 if rapdu.len() >= 18 { 113 let idm_bytes = &rapdu[10..18]; 114 let idm_str = idm_bytes 115 .iter() 116 .map(|b| format!("{:02X}", b)) 117 .collect::<String>(); 118 119 if let Ok(user_id) = nfc_sender.member_directory.inquire(&idm_str) { 120 if let Ok(current_presence) = nfc_sender.get_presence(&user_id) { 121 let toggled = NfcSender::toggled_presence(¤t_presence); 122 let _ = nfc_sender.update_presence(&user_id, toggled); 123 } 124 } 125 126 sleep(Duration::from_secs(2)); 127 } 128 } 129 } 130 131 sleep(Duration::from_millis(500)); 132 } 133 } 134 #[cfg(test)] 135 mod tests { 136 use super::*; 137 138 fn extract_idm(rapdu: &[u8]) -> Option<String> { 139 if rapdu.len() < 18 { 140 return None; 141 } 142 Some( 143 rapdu[10..18] 144 .iter() 145 .map(|b| format!("{:02X}", b)) 146 .collect::<String>(), 147 ) 148 } 149 150 #[test] 151 fn test_toggled_presence_in_lab_to_offline() { 152 let current = "inLab"; 153 let next = NfcSender::toggled_presence(current); 154 assert_eq!(next, "offline"); 155 } 156 157 #[test] 158 fn test_toggled_presence_offline_to_in_lab() { 159 let current = "offline"; 160 let next = NfcSender::toggled_presence(current); 161 assert_eq!(next, "inLab"); 162 } 163 164 #[test] 165 fn test_toggled_presence_unknown_status_defaults_to_in_lab() { 166 let current = "unknown"; 167 let next = NfcSender::toggled_presence(current); 168 assert_eq!(next, "inLab"); 169 } 170 171 #[test] 172 fn test_extract_idm_valid_rapdu() { 173 let mut rapdu = vec![0u8; 18]; 174 rapdu[10..18].copy_from_slice(&[0x01, 0x2E, 0x3F, 0x4A, 0x5B, 0x6C, 0x7D, 0x8E]); 175 176 let idm = extract_idm(&rapdu); 177 assert_eq!(idm, Some("012E3F4A5B6C7D8E".to_string())); 178 } 179 180 #[test] 181 fn test_extract_idm_short_rapdu_returns_none() { 182 let rapdu = vec![0u8; 17]; 183 let idm = extract_idm(&rapdu); 184 assert_eq!(idm, None); 185 } 186 } 187 #[cfg(test)] 188 mod integration_tests { 189 use super::*; 190 use wiremock::matchers::{header, method, path}; 191 use wiremock::{Mock, MockServer, ResponseTemplate}; 192 193 #[tokio::test] 194 async fn test_nfc_sender_api_flow() -> Result<(), Box<dyn std::error::Error>> { 195 let mock_server = MockServer::start().await; 196 197 Mock::given(method("GET")) 198 .and(path("/api/v1/presence/user_123/status")) 199 .and(header("Authorization", "Bearer mock_token")) 200 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ 201 "presence": "inLab", 202 "status_msg": "NFC toggle request sent", 203 "timestamp": 1625097600 204 }))) 205 .mount(&mock_server) 206 .await; 207 208 Mock::given(method("PUT")) 209 .and(path("/api/v1/presence/user_123/status")) 210 .and(header("Authorization", "Bearer mock_token")) 211 .respond_with(ResponseTemplate::new(200)) 212 .mount(&mock_server) 213 .await; 214 215 let sender = NfcSender { 216 core_url: mock_server.uri(), 217 token: "mock_token".to_string(), 218 member_directory: MemberDirectory::new(vec![]), 219 }; 220 221 let dummy_user = UserId { 222 id: "user_123".to_string(), 223 }; 224 225 let presence = sender.get_presence(&dummy_user)?; 226 assert_eq!(presence, "inLab"); 227 228 let toggled = NfcSender::toggled_presence(&presence); 229 let update_res = sender.update_presence(&dummy_user, toggled); 230 assert!(update_res.is_ok()); 231 232 Ok(()) 233 } 234 }