Implement devices API (#20)
small improvements Cargo fmt Simplify insert and update methods Review feedback Remove has_device method calls Load all devices with a single db call Remove device as in logout Put all metadata on the same tree Create userdevice key fucntion Implement devices API Implement all the devices endpoints. There's a couple of pending tasks: - Integrate the "logout" logic once it lands to master (this should remove the given device from the database). - Track and store last seen timestamp and IP. Co-authored-by: timokoesters <timo@koesters.xyz> Co-authored-by: Guillem Nieto <gnieto.talo@gmail.com>
This commit is contained in:
		
							parent
							
								
									720d48bd67
								
							
						
					
					
						commit
						ed9b544ace
					
				
					 4 changed files with 190 additions and 26 deletions
				
			
		|  | @ -13,6 +13,9 @@ use ruma_client_api::{ | ||||||
|         alias::{create_alias, delete_alias, get_alias}, |         alias::{create_alias, delete_alias, get_alias}, | ||||||
|         capabilities::get_capabilities, |         capabilities::get_capabilities, | ||||||
|         config::{get_global_account_data, set_global_account_data}, |         config::{get_global_account_data, set_global_account_data}, | ||||||
|  |         device::{ | ||||||
|  |             self, delete_device, delete_devices, get_device, get_devices, update_device, | ||||||
|  |         }, | ||||||
|         directory::{ |         directory::{ | ||||||
|             self, get_public_rooms, get_public_rooms_filtered, get_room_visibility, |             self, get_public_rooms, get_public_rooms_filtered, get_room_visibility, | ||||||
|             set_room_visibility, |             set_room_visibility, | ||||||
|  | @ -52,7 +55,7 @@ use ruma_events::{ | ||||||
|     room::{canonical_alias, guest_access, history_visibility, join_rules, member, redaction}, |     room::{canonical_alias, guest_access, history_visibility, join_rules, member, redaction}, | ||||||
|     EventJson, EventType, |     EventJson, EventType, | ||||||
| }; | }; | ||||||
| use ruma_identifiers::{RoomAliasId, RoomId, RoomVersionId, UserId}; | use ruma_identifiers::{DeviceId, RoomAliasId, RoomId, RoomVersionId, UserId}; | ||||||
| use serde_json::{json, value::RawValue}; | use serde_json::{json, value::RawValue}; | ||||||
| 
 | 
 | ||||||
| use crate::{server_server, utils, Database, MatrixResult, Ruma}; | use crate::{server_server, utils, Database, MatrixResult, Ruma}; | ||||||
|  | @ -173,7 +176,7 @@ pub fn register_route( | ||||||
| 
 | 
 | ||||||
|     // Generate new device id if the user didn't specify one
 |     // Generate new device id if the user didn't specify one
 | ||||||
|     let device_id = body |     let device_id = body | ||||||
|         .device_id |         .device_id.clone() | ||||||
|         .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH)); |         .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH)); | ||||||
| 
 | 
 | ||||||
|     // Generate new token for the device
 |     // Generate new token for the device
 | ||||||
|  | @ -181,7 +184,7 @@ pub fn register_route( | ||||||
| 
 | 
 | ||||||
|     // Add device
 |     // Add device
 | ||||||
|     db.users |     db.users | ||||||
|         .create_device(&user_id, &device_id, &token) |         .create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone()) | ||||||
|         .unwrap(); |         .unwrap(); | ||||||
| 
 | 
 | ||||||
|     // Initial data
 |     // Initial data
 | ||||||
|  | @ -300,6 +303,7 @@ pub fn login_route( | ||||||
|     let device_id = body |     let device_id = body | ||||||
|         .body |         .body | ||||||
|         .device_id |         .device_id | ||||||
|  |         .clone() | ||||||
|         .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH)); |         .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH)); | ||||||
| 
 | 
 | ||||||
|     // Generate a new token for the device
 |     // Generate a new token for the device
 | ||||||
|  | @ -307,7 +311,7 @@ pub fn login_route( | ||||||
| 
 | 
 | ||||||
|     // Add device
 |     // Add device
 | ||||||
|     db.users |     db.users | ||||||
|         .create_device(&user_id, &device_id, &token) |         .create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone()) | ||||||
|         .unwrap(); |         .unwrap(); | ||||||
| 
 | 
 | ||||||
|     MatrixResult(Ok(login::Response { |     MatrixResult(Ok(login::Response { | ||||||
|  | @ -2430,6 +2434,93 @@ pub fn get_content_thumbnail_route( | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #[get("/_matrix/client/r0/devices", data = "<body>")] | ||||||
|  | pub fn get_devices_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<get_devices::Request>, | ||||||
|  | ) -> MatrixResult<get_devices::Response> { | ||||||
|  |     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||||
|  | 
 | ||||||
|  |     let devices = db | ||||||
|  |         .users | ||||||
|  |         .all_devices_metadata(user_id) | ||||||
|  |         .map(|r| r.unwrap()) | ||||||
|  |         .collect::<Vec<device::Device>>(); | ||||||
|  | 
 | ||||||
|  |     MatrixResult(Ok(get_devices::Response { devices })) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[get("/_matrix/client/r0/devices/<device_id>", data = "<body>")] | ||||||
|  | pub fn get_device_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<get_device::Request>, | ||||||
|  |     device_id: DeviceId, | ||||||
|  | ) -> MatrixResult<get_device::Response> { | ||||||
|  |     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||||
|  |     let device = db.users.get_device_metadata(&user_id, &device_id).unwrap(); | ||||||
|  | 
 | ||||||
|  |     match device { | ||||||
|  |         None => MatrixResult(Err(Error { | ||||||
|  |             kind: ErrorKind::NotFound, | ||||||
|  |             message: "Device not found".to_string(), | ||||||
|  |             status_code: http::StatusCode::NOT_FOUND, | ||||||
|  |         })), | ||||||
|  |         Some(device) => MatrixResult(Ok(get_device::Response { device })), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[put("/_matrix/client/r0/devices/<device_id>", data = "<body>")] | ||||||
|  | pub fn update_device_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<update_device::Request>, | ||||||
|  |     device_id: DeviceId, | ||||||
|  | ) -> MatrixResult<update_device::Response> { | ||||||
|  |     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||||
|  |     let device = db.users.get_device_metadata(&user_id, &device_id).unwrap(); | ||||||
|  | 
 | ||||||
|  |     match device { | ||||||
|  |         None => MatrixResult(Err(Error { | ||||||
|  |             kind: ErrorKind::NotFound, | ||||||
|  |             message: "Device not found".to_string(), | ||||||
|  |             status_code: http::StatusCode::NOT_FOUND, | ||||||
|  |         })), | ||||||
|  |         Some(mut device) => { | ||||||
|  |             device.display_name = body.display_name.clone(); | ||||||
|  | 
 | ||||||
|  |             db.users | ||||||
|  |                 .update_device_metadata(&user_id, &device_id, &device) | ||||||
|  |                 .unwrap(); | ||||||
|  | 
 | ||||||
|  |             MatrixResult(Ok(update_device::Response)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[delete("/_matrix/client/r0/devices/<device_id>", data = "<body>")] | ||||||
|  | pub fn delete_device_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<delete_device::Request>, | ||||||
|  |     device_id: DeviceId, | ||||||
|  | ) -> MatrixResult<delete_device::Response> { | ||||||
|  |     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||||
|  |     db.users.remove_device(&user_id, &device_id).unwrap(); | ||||||
|  | 
 | ||||||
|  |     MatrixResult(Ok(delete_device::Response)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[post("/_matrix/client/r0/delete_devices", data = "<body>")] | ||||||
|  | pub fn delete_devices_route( | ||||||
|  |     db: State<'_, Database>, | ||||||
|  |     body: Ruma<delete_devices::Request>, | ||||||
|  | ) -> MatrixResult<delete_devices::Response> { | ||||||
|  |     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||||
|  |     for device_id in &body.devices { | ||||||
|  |         db.users.remove_device(&user_id, &device_id).unwrap() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     MatrixResult(Ok(delete_devices::Response)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[options("/<_segments..>")] | #[options("/<_segments..>")] | ||||||
| pub fn options_route( | pub fn options_route( | ||||||
|     _segments: rocket::http::uri::Segments<'_>, |     _segments: rocket::http::uri::Segments<'_>, | ||||||
|  |  | ||||||
|  | @ -56,10 +56,10 @@ impl Database { | ||||||
|             ), |             ), | ||||||
|             users: users::Users { |             users: users::Users { | ||||||
|                 userid_password: db.open_tree("userid_password").unwrap(), |                 userid_password: db.open_tree("userid_password").unwrap(), | ||||||
|                 userdeviceids: db.open_tree("userdeviceids").unwrap(), |  | ||||||
|                 userid_displayname: db.open_tree("userid_displayname").unwrap(), |                 userid_displayname: db.open_tree("userid_displayname").unwrap(), | ||||||
|                 userid_avatarurl: db.open_tree("userid_avatarurl").unwrap(), |                 userid_avatarurl: db.open_tree("userid_avatarurl").unwrap(), | ||||||
|                 userdeviceid_token: db.open_tree("userdeviceid_token").unwrap(), |                 userdeviceid_token: db.open_tree("userdeviceid_token").unwrap(), | ||||||
|  |                 userdeviceid_metadata: db.open_tree("userdeviceid_metadata").unwrap(), | ||||||
|                 token_userdeviceid: db.open_tree("token_userdeviceid").unwrap(), |                 token_userdeviceid: db.open_tree("token_userdeviceid").unwrap(), | ||||||
|                 onetimekeyid_onetimekeys: db.open_tree("onetimekeyid_onetimekeys").unwrap(), |                 onetimekeyid_onetimekeys: db.open_tree("onetimekeyid_onetimekeys").unwrap(), | ||||||
|                 userdeviceid_devicekeys: db.open_tree("userdeviceid_devicekeys").unwrap(), |                 userdeviceid_devicekeys: db.open_tree("userdeviceid_devicekeys").unwrap(), | ||||||
|  |  | ||||||
|  | @ -1,16 +1,19 @@ | ||||||
| use crate::{utils, Error, Result}; | use crate::{utils, Error, Result}; | ||||||
| use js_int::UInt; | use js_int::UInt; | ||||||
| use ruma_client_api::r0::keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey}; | use ruma_client_api::r0::{ | ||||||
|  |     device::Device, | ||||||
|  |     keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey}, | ||||||
|  | }; | ||||||
| use ruma_events::{to_device::AnyToDeviceEvent, EventJson, EventType}; | use ruma_events::{to_device::AnyToDeviceEvent, EventJson, EventType}; | ||||||
| use ruma_identifiers::{DeviceId, UserId}; | use ruma_identifiers::{DeviceId, UserId}; | ||||||
| use std::{collections::BTreeMap, convert::TryFrom}; | use std::{collections::BTreeMap, convert::TryFrom, time::SystemTime}; | ||||||
| 
 | 
 | ||||||
| pub struct Users { | pub struct Users { | ||||||
|     pub(super) userid_password: sled::Tree, |     pub(super) userid_password: sled::Tree, | ||||||
|     pub(super) userid_displayname: sled::Tree, |     pub(super) userid_displayname: sled::Tree, | ||||||
|     pub(super) userid_avatarurl: sled::Tree, |     pub(super) userid_avatarurl: sled::Tree, | ||||||
|     pub(super) userdeviceids: sled::Tree, |  | ||||||
|     pub(super) userdeviceid_token: sled::Tree, |     pub(super) userdeviceid_token: sled::Tree, | ||||||
|  |     pub(super) userdeviceid_metadata: sled::Tree, // This is also used to check if a device exists
 | ||||||
|     pub(super) token_userdeviceid: sled::Tree, |     pub(super) token_userdeviceid: sled::Tree, | ||||||
| 
 | 
 | ||||||
|     pub(super) onetimekeyid_onetimekeys: sled::Tree, // OneTimeKeyId = UserId + AlgorithmAndDeviceId
 |     pub(super) onetimekeyid_onetimekeys: sled::Tree, // OneTimeKeyId = UserId + AlgorithmAndDeviceId
 | ||||||
|  | @ -105,25 +108,40 @@ impl Users { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Adds a new device to a user.
 |     /// Adds a new device to a user.
 | ||||||
|     pub fn create_device(&self, user_id: &UserId, device_id: &DeviceId, token: &str) -> Result<()> { |     pub fn create_device( | ||||||
|  |         &self, | ||||||
|  |         user_id: &UserId, | ||||||
|  |         device_id: &DeviceId, | ||||||
|  |         token: &str, | ||||||
|  |         initial_device_display_name: Option<String>, | ||||||
|  |     ) -> Result<()> { | ||||||
|         if !self.exists(user_id)? { |         if !self.exists(user_id)? { | ||||||
|             return Err(Error::BadRequest( |             return Err(Error::BadRequest( | ||||||
|                 "tried to create device for nonexistent user", |                 "tried to create device for nonexistent user", | ||||||
|             )); |             )); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let mut key = user_id.to_string().as_bytes().to_vec(); |         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||||
|         key.push(0xff); |         userdeviceid.push(0xff); | ||||||
|         key.extend_from_slice(device_id.as_bytes()); |         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||||
| 
 | 
 | ||||||
|         self.userdeviceids.insert(key, &[])?; |         self.userdeviceid_metadata.insert( | ||||||
|  |             userdeviceid, | ||||||
|  |             serde_json::to_string(&Device { | ||||||
|  |                 device_id: device_id.clone(), | ||||||
|  |                 display_name: initial_device_display_name, | ||||||
|  |                 last_seen_ip: None, // TODO
 | ||||||
|  |                 last_seen_ts: Some(SystemTime::now()), | ||||||
|  |             })? | ||||||
|  |             .as_bytes(), | ||||||
|  |         )?; | ||||||
| 
 | 
 | ||||||
|         self.set_token(user_id, device_id, token)?; |         self.set_token(user_id, device_id, token)?; | ||||||
| 
 | 
 | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Removes a device from a user
 |     /// Removes a device from a user.
 | ||||||
|     pub fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()> { |     pub fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()> { | ||||||
|         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); |         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||||
|         userdeviceid.push(0xff); |         userdeviceid.push(0xff); | ||||||
|  | @ -147,8 +165,7 @@ impl Users { | ||||||
| 
 | 
 | ||||||
|         // TODO: Remove onetimekeys
 |         // TODO: Remove onetimekeys
 | ||||||
| 
 | 
 | ||||||
|         // Remove the device
 |         self.userdeviceid_metadata.remove(&userdeviceid)?; | ||||||
|         self.userdeviceids.remove(userdeviceid)?; |  | ||||||
| 
 | 
 | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  | @ -157,14 +174,18 @@ impl Users { | ||||||
|     pub fn all_device_ids(&self, user_id: &UserId) -> impl Iterator<Item = Result<DeviceId>> { |     pub fn all_device_ids(&self, user_id: &UserId) -> impl Iterator<Item = Result<DeviceId>> { | ||||||
|         let mut prefix = user_id.to_string().as_bytes().to_vec(); |         let mut prefix = user_id.to_string().as_bytes().to_vec(); | ||||||
|         prefix.push(0xff); |         prefix.push(0xff); | ||||||
|         self.userdeviceids.scan_prefix(prefix).keys().map(|bytes| { |         // All devices have metadata
 | ||||||
|             Ok(utils::string_from_bytes( |         self.userdeviceid_metadata | ||||||
|                 &*bytes? |             .scan_prefix(prefix) | ||||||
|                     .rsplit(|&b| b == 0xff) |             .keys() | ||||||
|                     .next() |             .map(|bytes| { | ||||||
|                     .ok_or(Error::BadDatabase("userdeviceid is invalid"))?, |                 Ok(utils::string_from_bytes( | ||||||
|             )?) |                     &*bytes? | ||||||
|         }) |                         .rsplit(|&b| b == 0xff) | ||||||
|  |                         .next() | ||||||
|  |                         .ok_or(Error::BadDatabase("userdeviceid is invalid"))?, | ||||||
|  |                 )?) | ||||||
|  |             }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Replaces the access token of one device.
 |     /// Replaces the access token of one device.
 | ||||||
|  | @ -173,7 +194,8 @@ impl Users { | ||||||
|         userdeviceid.push(0xff); |         userdeviceid.push(0xff); | ||||||
|         userdeviceid.extend_from_slice(device_id.as_bytes()); |         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||||
| 
 | 
 | ||||||
|         if self.userdeviceids.get(&userdeviceid)?.is_none() { |         // All devices have metadata
 | ||||||
|  |         if self.userdeviceid_metadata.get(&userdeviceid)?.is_none() { | ||||||
|             return Err(Error::BadRequest( |             return Err(Error::BadRequest( | ||||||
|                 "Tried to set token for nonexistent device", |                 "Tried to set token for nonexistent device", | ||||||
|             )); |             )); | ||||||
|  | @ -203,7 +225,8 @@ impl Users { | ||||||
|         key.push(0xff); |         key.push(0xff); | ||||||
|         key.extend_from_slice(device_id.as_bytes()); |         key.extend_from_slice(device_id.as_bytes()); | ||||||
| 
 | 
 | ||||||
|         if self.userdeviceids.get(&key)?.is_none() { |         // All devices have metadata
 | ||||||
|  |         if self.userdeviceid_metadata.get(&key)?.is_none() { | ||||||
|             return Err(Error::BadRequest( |             return Err(Error::BadRequest( | ||||||
|                 "Tried to set token for nonexistent device", |                 "Tried to set token for nonexistent device", | ||||||
|             )); |             )); | ||||||
|  | @ -396,4 +419,49 @@ impl Users { | ||||||
| 
 | 
 | ||||||
|         Ok(events) |         Ok(events) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     pub fn update_device_metadata( | ||||||
|  |         &self, | ||||||
|  |         user_id: &UserId, | ||||||
|  |         device_id: &DeviceId, | ||||||
|  |         device: &Device, | ||||||
|  |     ) -> Result<()> { | ||||||
|  |         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||||
|  |         userdeviceid.push(0xff); | ||||||
|  |         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||||
|  | 
 | ||||||
|  |         if self.userdeviceid_metadata.get(userdeviceid)?.is_none() { | ||||||
|  |             return Err(Error::BadRequest("device does not exist")); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.userdeviceid_metadata | ||||||
|  |             .insert(userdeviceid, serde_json::to_string(device)?.as_bytes())?; | ||||||
|  | 
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Get device metadata.
 | ||||||
|  |     pub fn get_device_metadata( | ||||||
|  |         &self, | ||||||
|  |         user_id: &UserId, | ||||||
|  |         device_id: &DeviceId, | ||||||
|  |     ) -> Result<Option<Device>> { | ||||||
|  |         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||||
|  |         userdeviceid.push(0xff); | ||||||
|  |         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||||
|  | 
 | ||||||
|  |         self.userdeviceid_metadata | ||||||
|  |             .get(&userdeviceid)? | ||||||
|  |             .map_or(Ok(None), |bytes| Ok(Some(serde_json::from_slice(&bytes)?))) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn all_devices_metadata(&self, user_id: &UserId) -> impl Iterator<Item = Result<Device>> { | ||||||
|  |         let mut key = user_id.to_string().as_bytes().to_vec(); | ||||||
|  |         key.push(0xff); | ||||||
|  | 
 | ||||||
|  |         self.userdeviceid_metadata | ||||||
|  |             .scan_prefix(key) | ||||||
|  |             .values() | ||||||
|  |             .map(|bytes| Ok(serde_json::from_slice::<Device>(&bytes?)?)) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -81,6 +81,11 @@ fn setup_rocket() -> rocket::Rocket { | ||||||
|                 client_server::create_content_route, |                 client_server::create_content_route, | ||||||
|                 client_server::get_content_route, |                 client_server::get_content_route, | ||||||
|                 client_server::get_content_thumbnail_route, |                 client_server::get_content_thumbnail_route, | ||||||
|  |                 client_server::get_devices_route, | ||||||
|  |                 client_server::get_device_route, | ||||||
|  |                 client_server::update_device_route, | ||||||
|  |                 client_server::delete_device_route, | ||||||
|  |                 client_server::delete_devices_route, | ||||||
|                 client_server::options_route, |                 client_server::options_route, | ||||||
|                 server_server::well_known_server, |                 server_server::well_known_server, | ||||||
|                 server_server::get_server_version, |                 server_server::get_server_version, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue