main.rs (7982B)
1 mod auth; 2 use auth::{AuthenticatedDevice, generate_secure_token, validate_device}; 3 4 use actix_web::{ 5 App, HttpRequest, HttpResponse, HttpServer, Responder, get, http::header::ContentType, 6 middleware, post, put, web, 7 }; 8 use log::debug; 9 use rcgen::generate_simple_self_signed; 10 use rustls::{ 11 ServerConfig, 12 pki_types::{CertificateDer, PrivateKeyDer}, 13 }; 14 use shared::*; 15 use std::sync::Mutex; 16 use std::{collections::HashMap, time::UNIX_EPOCH}; 17 18 struct AppState { 19 presences: Mutex<HashMap<String, LabPresence>>, 20 tokens: Mutex<HashMap<String, String>>, 21 } 22 23 /// simple handle 24 async fn index(req: HttpRequest) -> HttpResponse { 25 debug!("{req:?}"); 26 27 HttpResponse::Ok().content_type(ContentType::html()).body( 28 "<!DOCTYPE html><html><body>\ 29 <h1>Lab Presence</h1>\ 30 </body></html>", 31 ) 32 } 33 34 #[actix_web::main] 35 async fn main() -> std::io::Result<()> { 36 env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); 37 38 let config = load_rustls_config(); 39 40 let state = web::Data::new(AppState { 41 presences: Mutex::new(HashMap::new()), 42 tokens: Mutex::new(HashMap::new()), 43 }); 44 45 log::info!("starting HTTPS server at https://localhost:8443"); 46 47 HttpServer::new(move || { 48 App::new() 49 .app_data(state.clone()) 50 .wrap(middleware::Logger::default()) 51 .service(login) 52 .service(get_presence) 53 .service(put_presence) 54 .service(web::resource("/index.html").to(index)) 55 .service(web::redirect("/", "/index.html")) 56 }) 57 .bind_rustls_0_23("127.0.0.1:8443", config)? 58 .run() 59 .await 60 } 61 62 fn load_rustls_config() -> rustls::ServerConfig { 63 rustls::crypto::aws_lc_rs::default_provider() 64 .install_default() 65 .unwrap(); 66 67 let subject_alt_names = vec!["localhost".to_string(), "127.0.0.1".to_string()]; 68 let cert = generate_simple_self_signed(subject_alt_names).unwrap(); 69 70 let cert_chain = vec![CertificateDer::from(cert.cert.der().to_vec())]; 71 let key_der = PrivateKeyDer::Pkcs8(cert.signing_key.serialize_der().into()); 72 73 ServerConfig::builder() 74 .with_no_client_auth() 75 .with_single_cert(cert_chain, key_der) 76 .unwrap() 77 } 78 79 #[post("/api/v1/login")] 80 async fn login(data: web::Json<LoginRequest>, state: web::Data<AppState>) -> impl Responder { 81 if !validate_device(&data.device_id, &data.device_secret) { 82 return HttpResponse::Forbidden().json(ErrorResponse { 83 errcode: "M_FORBIDDEN".to_string(), 84 error: "authorization failed".to_string(), 85 }); 86 } 87 88 let token = generate_secure_token(); 89 let mut tokens = state.tokens.lock().unwrap(); 90 tokens.insert(token.clone(), data.device_id.clone()); 91 92 HttpResponse::Ok().json(LoginResponse { 93 access_token: token, 94 expires_in: 3600, 95 }) 96 } 97 98 #[get("/api/v1/presence/{userId}/status")] 99 async fn get_presence( 100 path: web::Path<String>, 101 _auth: AuthenticatedDevice, 102 state: web::Data<AppState>, 103 ) -> impl Responder { 104 let user_id = path.into_inner(); 105 let presences = state.presences.lock().unwrap(); 106 107 if let Some(presence) = presences.get(&user_id) { 108 HttpResponse::Ok().json(PresenceResponse { 109 presence: presence.presence().to_string(), 110 status_msg: presence.status_msg().unwrap_or("").to_string(), 111 timestamp: presence.timestamp(), 112 }) 113 } else { 114 HttpResponse::NotFound().json(ErrorResponse { 115 errcode: "M_UNKNOWN".to_string(), 116 error: "An unknown error occurred".to_string(), 117 }) 118 } 119 } 120 121 #[put("/api/v1/presence/{userId}/status")] 122 async fn put_presence( 123 path: web::Path<String>, 124 data: web::Json<PresenceUpdateRequest>, 125 _auth: AuthenticatedDevice, 126 state: web::Data<AppState>, 127 ) -> impl Responder { 128 let user_id = path.into_inner(); 129 let mut presences = state.presences.lock().unwrap(); 130 131 let status_msg = if data.status_msg.is_empty() { 132 None 133 } else { 134 Some(data.status_msg.clone()) 135 }; 136 137 if let Some(existing) = presences.get_mut(&user_id) { 138 match existing.update( 139 data.presence.clone(), 140 status_msg, 141 std::time::SystemTime::now() 142 .duration_since(UNIX_EPOCH) 143 .unwrap() 144 .as_secs() as u64, 145 ) { 146 Ok(_) => HttpResponse::Ok().json(serde_json::json!({})), 147 Err(e) => HttpResponse::BadRequest().json(ErrorResponse { 148 errcode: "M_BAD_JSON".to_string(), 149 error: e.to_string(), 150 }), 151 } 152 } else { 153 presences.insert( 154 user_id, 155 LabPresence::new( 156 data.presence.clone(), 157 status_msg, 158 std::time::SystemTime::now() 159 .duration_since(UNIX_EPOCH) 160 .unwrap() 161 .as_secs() as u64, 162 ), 163 ); 164 HttpResponse::Ok().json(serde_json::json!({})) 165 } 166 } 167 168 #[cfg(test)] 169 mod tests { 170 use super::*; 171 use actix_web::{App, test, web}; 172 use shared::{LoginRequest, LoginResponse, PresenceUpdateRequest}; 173 174 #[actix_web::test] 175 async fn test_auth_and_presence_flow() { 176 let state = web::Data::new(AppState { 177 presences: Mutex::new(HashMap::new()), 178 tokens: Mutex::new(HashMap::new()), 179 }); 180 181 let app = test::init_service( 182 App::new() 183 .app_data(state.clone()) 184 .service(login) 185 .service(get_presence) 186 .service(put_presence), 187 ) 188 .await; 189 190 let invalid_login_req = test::TestRequest::post() 191 .uri("/api/v1/login") 192 .set_json(&LoginRequest { 193 device_id: "nfc_reader_01".to_string(), 194 device_secret: "wrong_secret".to_string(), 195 }) 196 .to_request(); 197 198 let resp = test::call_service(&app, invalid_login_req).await; 199 assert_eq!(resp.status(), 403); 200 201 let valid_login_req = test::TestRequest::post() 202 .uri("/api/v1/login") 203 .set_json(&LoginRequest { 204 device_id: "nfc".to_string(), 205 device_secret: "nfc".to_string(), 206 }) 207 .to_request(); 208 209 let resp = test::call_service(&app, valid_login_req).await; 210 assert_eq!(resp.status(), 200); 211 212 let login_res: LoginResponse = test::read_body_json(resp).await; 213 let token = login_res.access_token; 214 assert!(!token.is_empty()); 215 216 let unauth_put_req = test::TestRequest::put() 217 .uri("/api/v1/presence/user_hash_123/status") 218 .set_json(&PresenceUpdateRequest { 219 presence: "online".to_string(), 220 status_msg: "In Lab".to_string(), 221 }) 222 .to_request(); 223 224 let resp = test::call_service(&app, unauth_put_req).await; 225 assert_eq!(resp.status(), 403); 226 227 let auth_put_req = test::TestRequest::put() 228 .uri("/api/v1/presence/user_hash_123/status") 229 .insert_header(("Authorization", format!("Bearer {}", token))) 230 .set_json(&PresenceUpdateRequest { 231 presence: "online".to_string(), 232 status_msg: "In Lab".to_string(), 233 }) 234 .to_request(); 235 236 let resp = test::call_service(&app, auth_put_req).await; 237 assert_eq!(resp.status(), 200); 238 239 let get_req = test::TestRequest::get() 240 .uri("/api/v1/presence/user_hash_123/status") 241 .insert_header(("Authorization", format!("Bearer {}", token))) 242 .to_request(); 243 244 let resp = test::call_service(&app, get_req).await; 245 assert_eq!(resp.status(), 200); 246 247 let presence_res: PresenceResponse = test::read_body_json(resp).await; 248 assert_eq!(presence_res.presence, "online"); 249 assert_eq!(presence_res.status_msg, "In Lab"); 250 } 251 }