diff --git a/Cargo.lock b/Cargo.lock index a194fa3cff..80af5670d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4134,6 +4134,7 @@ version = "0.5.0" dependencies = [ "anyhow", "anymap", + "base64 0.21.2", "futures", "futures-channel", "http", diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 6eb5c39491..c7f03d6ba1 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -21,3 +21,4 @@ log = "0.4" futures-channel = "0.3" anymap = "0.12" im = "15.1" +base64 = "0.21" diff --git a/crates/sdk/src/background_connection.rs b/crates/sdk/src/background_connection.rs index d864c6941b..ded847c739 100644 --- a/crates/sdk/src/background_connection.rs +++ b/crates/sdk/src/background_connection.rs @@ -303,11 +303,13 @@ impl BackgroundDbConnection { } pub fn subscribe(&self, queries: &[&str]) { + self.subscribe_owned(queries.iter().map(|&s| s.into()).collect()); + } + + pub fn subscribe_owned(&self, queries: Vec) { if let Err(e) = self.send_chan.unbounded_send(client_api_messages::Message { r#type: Some(client_api_messages::message::Type::Subscribe( - client_api_messages::Subscribe { - query_strings: queries.iter().map(|&s| s.into()).collect(), - }, + client_api_messages::Subscribe { query_strings: queries }, )), }) { // TODO: decide how to handle this error. Panic? Log? Return result? The only diff --git a/crates/sdk/src/callbacks.rs b/crates/sdk/src/callbacks.rs index 6bdd9ec4d1..3d8506372b 100644 --- a/crates/sdk/src/callbacks.rs +++ b/crates/sdk/src/callbacks.rs @@ -852,4 +852,19 @@ impl CredentialStore { self.credentials = Some(creds); } } + + /// Return the current connection's `Identity`, if one is stored. + pub(crate) fn identity(&self) -> Option { + self.credentials.as_ref().map(|creds| creds.identity.clone()) + } + + /// Return the current connection's private `Token`, if one is stored. + pub(crate) fn token(&self) -> Option { + self.credentials.as_ref().map(|creds| creds.token.clone()) + } + + /// Return the current connection's `Credentials`, if they are stored. + pub(crate) fn credentials(&self) -> Option { + self.credentials.clone() + } } diff --git a/crates/sdk/src/client_cache.rs b/crates/sdk/src/client_cache.rs index 3348c572c3..27517d2b10 100644 --- a/crates/sdk/src/client_cache.rs +++ b/crates/sdk/src/client_cache.rs @@ -107,7 +107,11 @@ impl TableCache { let client_api_messages::TableRowOperation { op, row_pk, row } = row_op; match bsatn::from_slice(&row) { Err(e) => { - log::error!("Error while deserializing row from TableRowOperation: {:?}", e); + log::error!( + "Error while deserializing row from TableRowOperation: {:?}. Row is {:?}", + e, + row + ); } Ok(value) => { if op_is_delete(op) { @@ -227,7 +231,11 @@ impl TableCache { match diff.remove(&row_pk) { None => match bsatn::from_slice(&row) { Err(e) => { - log::error!("Error while deserializing row from `TableRowOperation`: {:?}", e); + log::error!( + "Error while deserializing row from `TableRowOperation`: {:?}. Row is {:?}", + e, + row + ); } Ok(row) => { log::info!("Initializing table {:?}: got new row {:?}", T::TABLE_NAME, row); @@ -333,7 +341,11 @@ impl TableCache { ) -> Option> { match bsatn::from_slice(&row) { Err(e) => { - log::error!("Error while deserializing row from `TableRowOperation`: {:?}", e); + log::error!( + "Error while deserializing row from `TableRowOperation`: {:?}. Row is {:?}", + e, + row + ); None } Ok(row) => { diff --git a/crates/sdk/src/identity.rs b/crates/sdk/src/identity.rs index c535489932..21a68cf8f4 100644 --- a/crates/sdk/src/identity.rs +++ b/crates/sdk/src/identity.rs @@ -1,22 +1,68 @@ use crate::callbacks::CallbackId; use crate::global_connection::try_with_credential_store; -use anyhow::Result; +use anyhow::{anyhow, Result}; +use spacetimedb_lib::de::Deserialize; +use spacetimedb_lib::ser::Serialize; // TODO: impl ser/de for `Identity`, `Token`, `Credentials` so that clients can stash them // to disk and use them to re-connect. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] /// A unique public identifier for a client connected to a database. pub struct Identity { pub(crate) bytes: Vec, } -#[derive(Clone, Debug, PartialEq, Eq)] +impl Identity { + /// Get a reference to the bytes of this identity. + /// + /// This may be useful for saving the bytes to disk in order to reconnect + /// with the same identity, though client authors are encouraged + /// to use the BSATN `Serialize` and `Deserialize` traits + /// rather than saving bytes directly. + /// + /// Due to a current limitation in Spacetime's handling of tables which store identities, + /// filter methods for fields defined by the module to have type `Identity` + /// accept bytes, rather than an `Identity` structure. + /// As such, it is necessary to do e.g. + /// `MyTable::filter_by_identity(some_identity.bytes().to_owned())`. + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + + /// Construct an `Identity` containing the `bytes`. + /// + /// This method does not verify that `bytes` represents a valid identity. + pub fn from_bytes(bytes: Vec) -> Self { + Identity { bytes } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] /// A private access token for a client connected to a database. pub struct Token { pub(crate) string: String, } -#[derive(Clone, Debug, PartialEq, Eq)] +impl Token { + /// Get a reference to the string representation of this token. + /// + /// This may be useful for saving the string to disk in order to reconnect + /// with the same token, though client authors are encouraged + /// to use the BSATN `Serialize` and `Deserialize` traits + /// rather than saving the token string directly. + pub fn string(&self) -> &str { + &self.string + } + + /// Construct a token from its string representation. + /// + /// This method does not verify that `string` represents a valid token. + pub fn from_string(string: String) -> Self { + Token { string } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] /// Credentials, including a private access token, sufficient to authenticate a client /// connected to a database. pub struct Credentials { @@ -90,3 +136,34 @@ pub fn once_on_connect(callback: impl FnOnce(&Credentials) + Send + 'static) -> pub fn remove_on_connect(id: ConnectCallbackId) -> Result<()> { try_with_credential_store(|cred_store| cred_store.unregister_on_connect(id.id)) } + +/// Read the current connection's public `Identity`. +/// +/// Returns an error if: +/// - `connect` has not yet been called. +/// - We connected anonymously, and we have not yet received our credentials. +pub fn identity() -> Result { + try_with_credential_store(|cred_store| cred_store.identity().ok_or(anyhow!("Identity not yet received"))) + .and_then(|inner| inner) +} + +/// Read the current connection's private `Token`. +/// +/// Returns an error if: +/// - `connect` has not yet been called. +/// - We connected anonymously, and we have not yet received our credentials. +pub fn token() -> Result { + try_with_credential_store(|cred_store| cred_store.token().ok_or(anyhow!("Token not yet received"))) + .and_then(|inner| inner) +} + +/// Read the current connection's `Credentials`, +/// including a public `Identity` and a private `Token`. +/// +/// Returns an error if: +/// - `connect` has not yet been called. +/// - We connected anonymously, and we have not yet received our credentials. +pub fn credentials() -> Result { + try_with_credential_store(|cred_store| cred_store.credentials().ok_or(anyhow!("Credentials not yet received"))) + .and_then(|inner| inner) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index c988073c31..2616d3ab0c 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -44,6 +44,30 @@ pub use http; #[doc(hidden)] pub use spacetimedb_sats as sats; +/// Subscribe to a set of queries, +/// to be notified when rows which match those queries are altered. +/// +/// The `queries` should be a slice of strings representing SQL queries. +/// +/// A new call to `subscribe` (or [`subscribe_owned`]) will remove all previous subscriptions +/// and replace them with the new `queries`. +/// If any rows matched the previous subscribed queries but do not match the new queries, +/// those rows will be removed from the client cache, +/// and `TableType::on_delete` callbacks will be invoked for them. pub fn subscribe(queries: &[&str]) -> anyhow::Result<()> { try_with_connection(|conn| conn.subscribe(queries)) } + +/// Subscribe to a set of queries, +/// to be notified when rows which match those queries are altered. +/// +/// The `queries` should be a `Vec` of `String`s representing SQL queries. +/// +/// A new call to `subscribe_owned` (or [`subscribe`]) will remove all previous subscriptions +/// and replace them with the new `queries`. +/// If any rows matched the previous subscribed queries but do not match the new queries, +/// those rows will be removed from the client cache, +/// and `TableType::on_delete` callbacks will be invoked for them. +pub fn subscribe_owned(queries: Vec) -> anyhow::Result<()> { + try_with_connection(|conn| conn.subscribe_owned(queries)) +} diff --git a/crates/sdk/src/websocket.rs b/crates/sdk/src/websocket.rs index d2697f5faf..a98373effb 100644 --- a/crates/sdk/src/websocket.rs +++ b/crates/sdk/src/websocket.rs @@ -66,7 +66,6 @@ where >::Error: std::error::Error + Send + Sync + 'static, { let uri = make_uri(host, db_name)?; - println!("Uri: {:?}", uri); let mut req = IntoClientRequest::into_client_request(uri)?; request_insert_protocol_header(&mut req); request_insert_auth_header(&mut req, credentials); @@ -94,12 +93,15 @@ const AUTH_HEADER_KEY: &str = "Authorization"; fn request_insert_auth_header(req: &mut http::Request<()>, credentials: Option<&Credentials>) { // TODO: figure out how the token is supposed to be encoded in the request if let Some(Credentials { token, .. }) = credentials { + use base64::Engine; + + let auth_bytes = format!("token:{}", token.string); + let encoded = base64::prelude::BASE64_STANDARD.encode(auth_bytes); + let auth_header_val = format!("Basic {}", encoded); request_add_header( req, AUTH_HEADER_KEY, - token - .string - .clone() + auth_header_val .try_into() .expect("Failed to convert token to http HeaderValue"), )