diff --git a/examples/e02_transparent_guild_sharding/src/main.rs b/examples/e02_transparent_guild_sharding/src/main.rs index 288323e8b29..319b0b645d5 100644 --- a/examples/e02_transparent_guild_sharding/src/main.rs +++ b/examples/e02_transparent_guild_sharding/src/main.rs @@ -42,12 +42,15 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = env::var("DISCORD_TOKEN") + .expect("Expected a token in the environment") + .parse() + .expect("Invalid token"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // The total number of shards to use. The "current shard number" of a shard - that is, the // shard it is assigned to - is indexed at 0, while the total shard count is indexed at 1. diff --git a/examples/e07_shard_manager/src/main.rs b/examples/e07_shard_manager/src/main.rs index 53ddcedfd35..29fd9445f95 100644 --- a/examples/e07_shard_manager/src/main.rs +++ b/examples/e07_shard_manager/src/main.rs @@ -44,13 +44,16 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = env::var("DISCORD_TOKEN") + .expect("Expected a token in the environment") + .parse() + .expect("Invalid token"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // Here we clone a lock to the Shard Manager, and then move it into a new thread. The thread // will unlock the manager and print shards' status on a loop. diff --git a/examples/e10_gateway_intents/src/main.rs b/examples/e10_gateway_intents/src/main.rs index ea1ffeca732..8bdfd3112bb 100644 --- a/examples/e10_gateway_intents/src/main.rs +++ b/examples/e10_gateway_intents/src/main.rs @@ -33,13 +33,16 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = env::var("DISCORD_TOKEN") + .expect("Expected a token in the environment") + .parse() + .expect("Invalid token"); // Intents are a bitflag, bitwise operations can be used to dictate which intents to use let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; // Build our client. - let mut client = Client::builder(&token, intents) + let mut client = Client::builder(token, intents) .event_handler(Handler) .await .expect("Error creating client"); diff --git a/examples/e13_sqlite_database/src/main.rs b/examples/e13_sqlite_database/src/main.rs index f8bf5d4d2e9..efb3821eaba 100644 --- a/examples/e13_sqlite_database/src/main.rs +++ b/examples/e13_sqlite_database/src/main.rs @@ -72,7 +72,10 @@ impl EventHandler for Bot { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = std::env::var("DISCORD_TOKEN") + .expect("Expected a token in the environment") + .parse() + .expect("Invalid token"); // Initiate a connection to the database file, creating the file if required. let database = sqlx::sqlite::SqlitePoolOptions::new() @@ -96,6 +99,6 @@ async fn main() { | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(bot).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(bot).await.expect("Err creating client"); client.start().await.unwrap(); } diff --git a/examples/e15_webhook/src/main.rs b/examples/e15_webhook/src/main.rs index 946c18ba665..eaaa811a60f 100644 --- a/examples/e15_webhook/src/main.rs +++ b/examples/e15_webhook/src/main.rs @@ -5,7 +5,7 @@ use serenity::model::webhook::Webhook; #[tokio::main] async fn main() { // You don't need a token when you are only dealing with webhooks. - let http = Http::new(""); + let http = Http::new(); let webhook = Webhook::from_url(&http, "https://discord.com/api/webhooks/133742013374206969/hello-there-oPNtRN5UY5DVmBe7m1N0HE-replace-me-Dw9LRkgq3zI7LoW3Rb-k-q") .await .expect("Replace the webhook with your own"); diff --git a/src/gateway/client/mod.rs b/src/gateway/client/mod.rs index fda7949a395..f1010fc09dd 100644 --- a/src/gateway/client/mod.rs +++ b/src/gateway/client/mod.rs @@ -59,7 +59,7 @@ use crate::gateway::{ ShardManagerOptions, DEFAULT_WAIT_BETWEEN_SHARD_START, }; -use crate::http::{parse_token, Http}; +use crate::http::Http; use crate::internal::prelude::*; use crate::internal::tokio::spawn_named; use crate::model::gateway::GatewayIntents; @@ -70,7 +70,7 @@ use crate::model::user::OnlineStatus; /// A builder implementing [`IntoFuture`] building a [`Client`] to interact with Discord. #[must_use = "Builders do nothing unless they are awaited"] pub struct ClientBuilder { - token: SecretString, + token: Token, data: Option>, http: Arc, intents: GatewayIntents, @@ -94,8 +94,8 @@ impl ClientBuilder { /// **Panic**: If you have enabled the `framework`-feature (on by default), you must specify a /// framework via the [`Self::framework`] method, otherwise awaiting the builder will cause a /// panic. - pub fn new(token: &str, intents: GatewayIntents) -> Self { - Self::new_with_http(token, Arc::new(Http::new(token)), intents) + pub fn new(token: Token, intents: GatewayIntents) -> Self { + Self::new_with_http(token.clone(), Arc::new(Http::with_token(token)), intents) } /// Construct a new builder with a [`Http`] instance to calls methods on for the client @@ -104,9 +104,9 @@ impl ClientBuilder { /// **Panic**: If you have enabled the `framework`-feature (on by default), you must specify a /// framework via the [`Self::framework`] method, otherwise awaiting the builder will cause a /// panic. - pub fn new_with_http(token: &str, http: Arc, intents: GatewayIntents) -> Self { + pub fn new_with_http(token: Token, http: Arc, intents: GatewayIntents) -> Self { Self { - token: SecretString::new(parse_token(token)), + token, http, intents, data: None, @@ -416,8 +416,9 @@ impl IntoFuture for ClientBuilder { /// } /// /// # async fn run() -> Result<(), Box> { +/// let token = std::env::var("DISCORD_TOKEN")?.parse()?; /// let mut client = -/// Client::builder("my token here", GatewayIntents::default()).event_handler(Handler).await?; +/// Client::builder(token, GatewayIntents::default()).event_handler(Handler).await?; /// /// client.start().await?; /// # Ok(()) @@ -500,7 +501,7 @@ pub struct Client { } impl Client { - pub fn builder(token: &str, intents: GatewayIntents) -> ClientBuilder { + pub fn builder(token: Token, intents: GatewayIntents) -> ClientBuilder { ClientBuilder::new(token, intents) } @@ -543,8 +544,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = std::env::var("DISCORD_TOKEN")?.parse()?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start().await { /// println!("Err with client: {:?}", why); @@ -586,8 +587,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = std::env::var("DISCORD_TOKEN")?.parse()?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_autosharded().await { /// println!("Err with client: {:?}", why); @@ -633,8 +634,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = std::env::var("DISCORD_TOKEN")?.parse()?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shard(3, 5).await { /// println!("Err with client: {:?}", why); @@ -652,8 +653,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = std::env::var("DISCORD_TOKEN")?.parse()?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shard(0, 1).await { /// println!("Err with client: {:?}", why); @@ -695,8 +696,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = std::env::var("DISCORD_TOKEN")?.parse()?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shards(8).await { /// println!("Err with client: {:?}", why); @@ -738,8 +739,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = std::env::var("DISCORD_TOKEN")?.parse()?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shard_range(4..7, 10).await { /// println!("Err with client: {:?}", why); diff --git a/src/gateway/sharding/mod.rs b/src/gateway/sharding/mod.rs index b18697ef36d..a9ff3ca4fc3 100644 --- a/src/gateway/sharding/mod.rs +++ b/src/gateway/sharding/mod.rs @@ -113,7 +113,7 @@ pub struct Shard { // This acts as a timeout to determine if the shard has - for some reason - not started within // a decent amount of time. pub started: Instant, - token: SecretString, + token: Token, ws_url: Arc, resume_ws_url: Option, compression: TransportCompression, @@ -136,14 +136,13 @@ impl Shard { /// use serenity::gateway::{Shard, TransportCompression}; /// use serenity::model::gateway::{GatewayIntents, ShardInfo}; /// use serenity::model::id::ShardId; - /// use serenity::secret_string::SecretString; /// use tokio::sync::Mutex; /// # /// # use serenity::http::Http; /// # /// # async fn run() -> Result<(), Box> { /// # let http: Arc = unimplemented!(); - /// let token = SecretString::new(Arc::from(std::env::var("DISCORD_BOT_TOKEN")?)); + /// let token = std::env::var("DISCORD_BOT_TOKEN")?.parse()?; /// let shard_info = ShardInfo { /// id: ShardId(0), /// total: NonZeroU16::MIN, @@ -173,7 +172,7 @@ impl Shard { /// TLS error. pub async fn new( ws_url: Arc, - token: SecretString, + token: Token, shard_info: ShardInfo, intents: GatewayIntents, presence: Option, diff --git a/src/gateway/sharding/shard_manager.rs b/src/gateway/sharding/shard_manager.rs index 40896ed3518..b139e35b325 100644 --- a/src/gateway/sharding/shard_manager.rs +++ b/src/gateway/sharding/shard_manager.rs @@ -59,7 +59,6 @@ pub const DEFAULT_WAIT_BETWEEN_SHARD_START: Duration = Duration::from_secs(5); /// use std::env; /// use std::sync::{Arc, OnceLock}; /// -/// # use serenity::secret_string::SecretString; /// use serenity::gateway::client::EventHandler; /// use serenity::gateway::{ /// ShardManager, @@ -85,9 +84,10 @@ pub const DEFAULT_WAIT_BETWEEN_SHARD_START: Duration = Duration::from_secs(5); /// let ws_url = Arc::from(gateway_info.url); /// let event_handler = Arc::new(Handler); /// let max_concurrency = std::num::NonZeroU16::MIN; +/// let token = std::env::var("DISCORD_TOKEN")?.parse()?; /// /// ShardManager::new(ShardManagerOptions { -/// # token, +/// token, /// data, /// event_handler: Some(event_handler), /// raw_event_handler: None, @@ -380,7 +380,7 @@ impl Drop for ShardManager { } pub struct ShardManagerOptions { - pub token: SecretString, + pub token: Token, pub data: Arc, pub event_handler: Option>, pub raw_event_handler: Option>, diff --git a/src/gateway/sharding/shard_queuer.rs b/src/gateway/sharding/shard_queuer.rs index 8e358fb81bd..0fc202aae94 100644 --- a/src/gateway/sharding/shard_queuer.rs +++ b/src/gateway/sharding/shard_queuer.rs @@ -37,7 +37,7 @@ use crate::model::gateway::{GatewayIntents, ShardInfo}; /// A shard queuer instance _should_ be run in its own thread, due to the blocking nature of the /// loop itself as well as a 5 second thread sleep between shard starts. pub struct ShardQueuer { - pub(super) token: SecretString, + pub(super) token: Token, /// A copy of [`Client::data`] to be given to runners for contextual dispatching. /// /// [`Client::data`]: crate::Client::data diff --git a/src/http/client.rs b/src/http/client.rs index 199679c7fc6..5be76f2305c 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -3,9 +3,7 @@ use std::borrow::Cow; use std::cell::Cell; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use aformat::{aformat, CapStr}; use arrayvec::ArrayVec; use nonmax::{NonMaxU16, NonMaxU8}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; @@ -71,34 +69,25 @@ where /// ```rust /// # use serenity::http::HttpBuilder; /// # fn run() { -/// let http = -/// HttpBuilder::new("token").proxy("http://127.0.0.1:3000").ratelimiter_disabled(true).build(); +/// let http = HttpBuilder::new().proxy("http://127.0.0.1:3000").ratelimiter_disabled(true).build(); /// # } /// ``` #[must_use] +#[derive(Default)] pub struct HttpBuilder { client: Option, ratelimiter: Option, ratelimiter_disabled: bool, - token: Arc, + token: Option, proxy: Option>, application_id: Option, default_allowed_mentions: Option>, } impl HttpBuilder { - /// Construct a new builder to call methods on for the HTTP construction. The `token` will - /// automatically be prefixed "Bot " if not already. - pub fn new(token: &str) -> Self { - Self { - client: None, - ratelimiter: None, - ratelimiter_disabled: false, - token: parse_token(token), - proxy: None, - application_id: None, - default_allowed_mentions: None, - } + /// Construct a new builder. + pub fn new() -> Self { + Self::default() } /// Sets the application_id to use interactions. @@ -107,10 +96,9 @@ impl HttpBuilder { self } - /// Sets a token for the bot. If the token is not prefixed "Bot ", this method will - /// automatically do so. - pub fn token(mut self, token: &str) -> Self { - self.token = parse_token(token); + /// Sets an authorization token for the bot. + pub fn token(mut self, token: Token) -> Self { + self.token = Some(token); self } @@ -192,31 +180,20 @@ impl HttpBuilder { }); let ratelimiter = (!self.ratelimiter_disabled).then(|| { - self.ratelimiter - .unwrap_or_else(|| Ratelimiter::new(client.clone(), Arc::clone(&self.token))) + self.ratelimiter.unwrap_or_else(|| Ratelimiter::new(client.clone(), self.token.clone())) }); Http { client, ratelimiter, proxy: self.proxy, - token: SecretString::new(self.token), + token: self.token, application_id, default_allowed_mentions: self.default_allowed_mentions, } } } -pub fn parse_token(token: &str) -> Arc { - let token = token.trim(); - - if token.starts_with("Bot ") || token.starts_with("Bearer ") { - Arc::from(token) - } else { - Arc::from(aformat!("Bot {}", CapStr::<128>(token)).as_str()) - } -} - fn reason_into_header(reason: &str) -> Headers { let mut headers = Headers::new(); @@ -241,15 +218,29 @@ pub struct Http { pub(crate) client: Client, pub ratelimiter: Option, pub proxy: Option>, - token: SecretString, + token: Option, application_id: AtomicU64, pub default_allowed_mentions: Option>, } +impl Default for Http { + fn default() -> Self { + Self::new() + } +} + impl Http { + /// Construct an unauthorized HTTP client, with no token. Few things will work, but webhooks + /// are one exception. + #[must_use] + pub fn new() -> Self { + HttpBuilder::new().build() + } + + /// Construct an authorized HTTP client. #[must_use] - pub fn new(token: &str) -> Self { - HttpBuilder::new(token).build() + pub fn with_token(token: Token) -> Self { + HttpBuilder::new().token(token).build() } pub fn application_id(&self) -> Option { @@ -4392,7 +4383,11 @@ impl Http { ratelimiter.perform(req).await? } else { let request = req - .build(&self.client, self.token.expose_secret(), self.proxy.as_deref())? + .build( + &self.client, + self.token.as_ref().map(Token::expose_secret), + self.proxy.as_deref(), + )? .build()?; self.client.execute(request).await? }; diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs index 5bd48da4df0..a5dba221516 100644 --- a/src/http/ratelimiting.rs +++ b/src/http/ratelimiting.rs @@ -38,7 +38,6 @@ use std::borrow::Cow; use std::fmt; use std::str::{self, FromStr}; -use std::sync::Arc; use std::time::SystemTime; use dashmap::DashMap; @@ -85,7 +84,7 @@ pub struct Ratelimiter { client: Client, global: Mutex<()>, routes: DashMap, - token: SecretString, + token: Option, absolute_ratelimits: bool, ratelimit_callback: parking_lot::RwLock>, } @@ -105,13 +104,11 @@ impl fmt::Debug for Ratelimiter { impl Ratelimiter { /// Creates a new ratelimiter, with a shared [`reqwest`] client and the bot's token. - /// - /// The bot token must be prefixed with `"Bot "`. The ratelimiter does not prefix it. #[must_use] - pub fn new(client: Client, token: Arc) -> Self { + pub fn new(client: Client, token: Option) -> Self { Self { client, - token: SecretString::new(token), + token, global: Mutex::default(), routes: DashMap::new(), absolute_ratelimits: false, @@ -193,7 +190,11 @@ impl Ratelimiter { sleep(delay_time).await; } - let request = req.clone().build(&self.client, self.token.expose_secret(), None)?; + let request = req.clone().build( + &self.client, + self.token.as_ref().map(Token::expose_secret), + None, + )?; let response = self.client.execute(request.build()?).await?; // Check if the request got ratelimited by checking for status 429, and if so, sleep diff --git a/src/http/request.rs b/src/http/request.rs index 67137b35e09..218831a72ae 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -70,7 +70,7 @@ impl<'a> Request<'a> { pub fn build( self, client: &Client, - token: &str, + token: Option<&str>, proxy: Option<&str>, ) -> Result { let mut path = self.route.path().into_owned(); @@ -91,8 +91,12 @@ impl<'a> Request<'a> { let mut headers = self.headers.unwrap_or_default(); headers.insert(USER_AGENT, HeaderValue::from_static(constants::USER_AGENT)); - headers - .insert(AUTHORIZATION, HeaderValue::from_str(token).map_err(HttpError::InvalidHeader)?); + if let Some(token) = token { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(token).map_err(HttpError::InvalidHeader)?, + ); + } if let Some(multipart) = self.multipart { // Setting multipart adds the content-length header. diff --git a/src/internal/prelude.rs b/src/internal/prelude.rs index 981ce8a86a4..1daefde6f75 100644 --- a/src/internal/prelude.rs +++ b/src/internal/prelude.rs @@ -13,6 +13,6 @@ pub use super::utils::join_to_string; #[cfg(feature = "http")] pub use crate::error::Error; pub use crate::error::Result; -pub use crate::secret_string::SecretString; +pub use crate::secrets::{SecretString, Token}; pub type JsonMap = serde_json::Map; diff --git a/src/lib.rs b/src/lib.rs index 9bede7efd1f..c31d83ce019 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,7 @@ pub mod gateway; pub mod http; #[cfg(feature = "interactions_endpoint")] pub mod interactions_endpoint; -pub mod secret_string; +pub mod secrets; #[cfg(feature = "utils")] pub mod utils; @@ -142,10 +142,7 @@ pub mod all { pub use crate::interactions_endpoint::*; #[cfg(feature = "utils")] #[doc(no_inline)] - pub use crate::utils::{ - token::{validate as validate_token, InvalidToken}, - *, - }; + pub use crate::utils::*; // #[doc(no_inline)] // pub use crate::*; #[doc(no_inline)] diff --git a/src/secret_string.rs b/src/secret_string.rs deleted file mode 100644 index 9e6be5653f0..00000000000 --- a/src/secret_string.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::sync::Arc; - -/// A cheaply clonable, zeroed on drop, String. -/// -/// This is a simple newtype of `Arc` that uses [`zeroize::Zeroize`] on last drop to avoid -/// keeping it around in memory. -#[derive(Clone, serde::Deserialize, serde::Serialize)] -pub struct SecretString(Arc); - -impl SecretString { - #[must_use] - pub fn new(inner: Arc) -> Self { - Self(inner) - } - - #[must_use] - pub fn expose_secret(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Debug for SecretString { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - fmt.debug_tuple(std::any::type_name::()).field(&"").finish() - } -} - -impl zeroize::Zeroize for SecretString { - fn zeroize(&mut self) { - if let Some(string) = Arc::get_mut(&mut self.0) { - string.zeroize(); - } - } -} - -#[cfg(feature = "typesize")] -impl typesize::TypeSize for SecretString { - fn extra_size(&self) -> usize { - self.0.len() + (size_of::() * 2) - } -} diff --git a/src/secrets.rs b/src/secrets.rs new file mode 100644 index 00000000000..33f2a2bfbd8 --- /dev/null +++ b/src/secrets.rs @@ -0,0 +1,118 @@ +use std::fmt; +use std::str::FromStr; +use std::sync::Arc; + +use aformat::{aformat, CapStr}; + +/// A cheaply clonable, zeroed on drop, String. +/// +/// This is a simple newtype of `Arc` that uses [`zeroize::Zeroize`] on last drop to avoid +/// keeping it around in memory. +#[derive(Clone, Deserialize, Serialize)] +pub struct SecretString(Arc); + +impl SecretString { + #[must_use] + pub fn new(inner: Arc) -> Self { + Self(inner) + } + + #[must_use] + pub fn expose_secret(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Debug for SecretString { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt.debug_tuple(std::any::type_name::()).field(&"").finish() + } +} + +impl zeroize::Zeroize for SecretString { + fn zeroize(&mut self) { + if let Some(string) = Arc::get_mut(&mut self.0) { + string.zeroize(); + } + } +} + +#[cfg(feature = "typesize")] +impl typesize::TypeSize for Token { + fn extra_size(&self) -> usize { + self.0.len() + (size_of::() * 2) + } +} + +/// A type for securely storing and passing around a Discord token. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Token(SecretString); + +impl Token { + #[must_use] + pub fn expose_secret(&self) -> &str { + self.0.expose_secret() + } +} + +/// Parses a token and validates that is is likely in a valid format. +/// +/// This performs the following checks on a given token: +/// - Is not empty; +/// - Is optionally prefixed with `"Bot "` or `"Bearer "`; +/// - Contains 3 parts (split by the period char `'.'`); +/// +/// Note that a token prefixed with `"Bearer "` will have its prefix changed to `"Bot "` when +/// parsed. +/// +/// # Examples +/// +/// Validate that a token is valid and that a number of malformed tokens are actually invalid: +/// +/// ``` +/// use serenity::secrets::Token; +/// +/// // ensure a valid token is in fact a valid format: +/// assert!("Mjg4NzYwMjQxMzYzODc3ODg4.C_ikow.j3VupLBuE1QWZng3TMGH0z_UAwg".parse::().is_ok()); +/// +/// assert!("Mjg4NzYwMjQxMzYzODc3ODg4".parse::().is_err()); +/// assert!("".parse::().is_err()); +/// ``` +/// +/// # Errors +/// +/// Returns a [`InvalidToken`] when one of the above checks fail. The type of failure is not +/// specified. +impl FromStr for Token { + type Err = InvalidToken; + + fn from_str(s: &str) -> Result { + let token = s.trim().trim_start_matches("Bot ").trim_start_matches("Bearer "); + + let mut parts = token.split('.'); + let is_valid = parts.next().is_some_and(|p| !p.is_empty()) + && parts.next().is_some_and(|p| !p.is_empty()) + && parts.next().is_some_and(|p| !p.is_empty()) + && parts.next().is_none(); + + if is_valid { + Ok(Self(SecretString::new(Arc::from( + aformat!("Box {}", CapStr::<128>(token)).as_str(), + )))) + } else { + Err(InvalidToken) + } + } +} + +/// Error that can be returned by [`Token::from_str`]. +#[derive(Debug)] +pub struct InvalidToken; + +impl std::error::Error for InvalidToken {} + +impl fmt::Display for InvalidToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("The provided token was invalid") + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 197941b4cc2..7d4e2e15460 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -11,8 +11,6 @@ mod message_builder; #[cfg(feature = "collector")] mod quick_modal; -pub mod token; - use std::num::NonZeroU16; #[cfg(feature = "gateway")] @@ -26,8 +24,6 @@ use url::Url; pub use self::custom_message::CustomMessage; pub use self::message_builder::{Content, ContentModifier, EmbedMessageBuilding, MessageBuilder}; -#[doc(inline)] -pub use self::token::validate as validate_token; use crate::model::prelude::*; /// Retrieves the "code" part of an invite out of a URL. diff --git a/src/utils/token.rs b/src/utils/token.rs deleted file mode 100644 index 6059ebe9296..00000000000 --- a/src/utils/token.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Utilities to parse and validate Discord tokens. - -use std::{fmt, str}; - -/// Validates that a token is likely in a valid format. -/// -/// This performs the following checks on a given token: -/// - Is not empty; -/// - Contains 3 parts (split by the period char `'.'`); -/// - The second part of the token is at least 6 characters long; -/// -/// # Examples -/// -/// Validate that a token is valid and that a number of malformed tokens are actually invalid: -/// -/// ``` -/// use serenity::utils::token::validate; -/// -/// // ensure a valid token is in fact a valid format: -/// assert!(validate("Mjg4NzYwMjQxMzYzODc3ODg4.C_ikow.j3VupLBuE1QWZng3TMGH0z_UAwg").is_ok()); -/// -/// assert!(validate("Mjg4NzYwMjQxMzYzODc3ODg4").is_err()); -/// assert!(validate("").is_err()); -/// ``` -/// -/// # Errors -/// -/// Returns a [`InvalidToken`] when one of the above checks fail. The type of failure is not -/// specified. -pub fn validate(token: &str) -> Result<(), InvalidToken> { - // Tokens can be preceded by "Bot " (that's how the Discord API expects them) - let mut parts = token.trim_start_matches("Bot ").split('.'); - - let is_valid = parts.next().is_some_and(|p| !p.is_empty()) - && parts.next().is_some_and(|p| !p.is_empty()) - && parts.next().is_some_and(|p| !p.is_empty()) - && parts.next().is_none(); - - if is_valid { - Ok(()) - } else { - Err(InvalidToken) - } -} - -/// Error that can be return by [`validate`]. -#[derive(Debug)] -pub struct InvalidToken; - -impl std::error::Error for InvalidToken {} - -impl fmt::Display for InvalidToken { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("The provided token was invalid") - } -}