Compare commits

..

10 Commits

Author SHA1 Message Date
Gnome!
18c282e676 Respect the port number from VoiceServerUpdate (#285)
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (beta, beta) (push) Has been cancelled
CI / Test (nightly, nightly) (push) Has been cancelled
CI / Test (stable) (push) Has been cancelled
CI / Test (true, Windows, windows-latest) (push) Has been cancelled
CI / Test (true, driver tungstenite rustls, driver only) (push) Has been cancelled
CI / Test (true, gateway serenity tungstenite rustls, gateway only) (push) Has been cancelled
CI / Test (true, macOS, macOS-latest) (push) Has been cancelled
CI / Build docs (push) Has been cancelled
CI / Examples (push) Has been cancelled
Publish docs / Publish docs (push) Has been cancelled
2025-07-26 00:00:23 +01:00
Sapphire
c910d7087d Playlist fixes, add YoutubeDl::get_stream function (#279)
* Fix(ytdl): Return all results when querying a URL

* Publicly export `songbird::input::metadata::ytdl::Output`

Closes: https://github.com/serenity-rs/songbird/issues/277

* Make `YoutubeDl::query` public

* Abstract getting streams from YoutubeDl into functions `get_stream` and `get_streams`

* fmt, remove get_streams

* Fix doc comment

* fixup doc comments, export `metadata::ytdl::Output` as `YoutubeDlOutput`

* fmt

* export metadata

* fixup! fixup doc comments, export `metadata::ytdl::Output` as `YoutubeDlOutput`
2025-05-20 18:59:26 +01:00
Gnome!
8956352f13 Fix clippy warnings (#275) 2025-05-05 13:58:15 +01:00
Astro
64868e7213 Driver: Update the head and tail offsets returned from decrypt_*_in_place(). (#281)
* Correct the head and tail offsets returned from decrypt_*_in_place().

* Incorporate formatting changes from `cargo make ready`
2025-05-05 08:11:11 +01:00
Gnome!
5ba5170c91 Remove duplicate lookup in get_or_insert_inner (#273) 2025-02-23 13:02:04 +00:00
Gnome!
ff7bd4a9ee Remove simd-json mention from top level docs (#271) 2025-02-22 16:22:12 +00:00
Kyle Simpson
84accb0e3a Chore: Release v0.5.0 2025-02-21 22:05:58 +00:00
Kyle Simpson
e09843ded7 Chore: quick respin on twilight-example.
Feature unification in workspaces strikes again.
2025-02-21 16:28:39 +00:00
Kyle Simpson
d0b7fbb911 Docs: remove unused link. 2025-02-21 16:14:49 +00:00
Kyle Simpson
f05b262dfe Driver: Remove legacy encryption algorithms.
There is probably some followup cleanup which can be done, but keeping
the general structure intact might help if there are future changes on
available (non-E2EE) algorithms.
2025-02-21 16:02:16 +00:00
24 changed files with 259 additions and 402 deletions

View File

@@ -107,7 +107,7 @@ jobs:
sudo apt-get install -y libopus-dev sudo apt-get install -y libopus-dev
- name: Build docs - name: Build docs
env: env:
RUSTDOCFLAGS: -D broken_intra_doc_links RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links
run: | run: |
cargo doc --no-deps --features full-doc cargo doc --no-deps --features full-doc

View File

@@ -34,7 +34,7 @@ jobs:
- name: Build docs - name: Build docs
env: env:
RUSTDOCFLAGS: -D broken_intra_doc_links RUSTDOCFLAGS: -D rustdoc::broken_intra_doc_links
run: | run: |
cargo doc --no-deps --features full-doc cargo doc --no-deps --features full-doc

View File

@@ -1,5 +1,75 @@
# Changelog # Changelog
## [v0.5.0] — 2025-02-21 — **Starling**
Starlings are social birds, known for chittering loudly (often sounding like human speech!) and flocking en masse. Iridescent and speckled like the night sky after twilight, their murmurations make for a beautiful evening sight.
This is a smaller breaking release which adds support for the latest version of [*twilight*](https://twilight.rs/) (v0.16), with a few main breaking changes:
- `TrackHandle`s now store an arbitrary `Arc<dyn Any>`, rather than a `TypeMap`. You can set this field to any desired type during track creation using the `Track::new_with_data` API.
- Legacy encryption modes `"xsalsa20_poly1305"`, `"xsalsa20_poly1305_suffix"`, and `"xsalsa20_poly1305_lite"` have been removed. If you were explicitly specifying these then please change over to one of the new modes. Discord have ceased serving voice connections with these parameters.
- `simd-json` support has been removed from the library.
Thanks to the following for their contributions:
- [@decahedron1]
- [@DPlayer234]
- [@Erk-]
- [@FelixMcFelix]
- [@GnomedDev]
### Added
- Driver: Support `tokio-websockets` ([@decahedron1]) [c:c4331c4]
### Changed
- Chore: quick respin on twilight-example. ([@FelixMcFelix]) [c:e09843d]
- Chore: clippy appeasement ([@FelixMcFelix]) [c:47cf0b2]
- Chore: Bump rubato->0.16 ([@FelixMcFelix]) [c:5d320a3]
- Chore: Bump rand->0.9, tokio-tungstenite->0.26 ([@FelixMcFelix]) [c:b39ab98]
- Support for Twilight 0.16 ([@Erk-]) [c:b46a568]
- Update `tokio-tungstenite` dependency to 0.24 ([@DPlayer234]) [c:1c52e6e]
- Update to DashMap 6 ([@GnomedDev]) [c:9a244ba]
- Bump dependencies ([@GnomedDev]) [c:e9b2243]
- Chore: Update `tokio-websockets` to v0.7 ([@decahedron1]) [c:3bc132f]
- Tracks: Replace `RwLock<TypeMap>` data store with `Arc<dyn Any>` ([@GnomedDev]) [c:0d6a226]
- Allow borrowed strings for YoutubeDl ([@GnomedDev]) [c:2bcc522]
- Deps: Replace OnceCell with std::sync::OnceLock ([@GnomedDev]) [c:743a1d2]
### Fixed
- Docs: remove unused link. ([@FelixMcFelix]) [c:d0b7fbb]
- Remove unnecessary simd-json feature gate ([@GnomedDev]) [c:b435e16]
### Removed
- Driver: Remove legacy encryption algorithms. ([@FelixMcFelix]) [c:f05b262]
- Remove Simd-json ([@Erk-]) [c:c81f2a9]
<!-- COMPARISONS -->
[v0.5.0]: https://github.com/serenity-rs/songbird/compare/v0.4.6...v0.5.0
<!-- COMMITS -->
[c:e09843d]: https://github.com/serenity-rs/songbird/commit/e09843ded7d8a492159ad62854a88c17d013703a
[c:d0b7fbb]: https://github.com/serenity-rs/songbird/commit/d0b7fbb911931b9ad9bc9988bef48d79650e11d6
[c:f05b262]: https://github.com/serenity-rs/songbird/commit/f05b262dfe25c35c9ac010c53cce54b6ca464581
[c:47cf0b2]: https://github.com/serenity-rs/songbird/commit/47cf0b27ebe5587b23d7fe8b8e125ae0ccde304b
[c:5d320a3]: https://github.com/serenity-rs/songbird/commit/5d320a394b2b00cb4dd04636c0c98aabdbbb7923
[c:b39ab98]: https://github.com/serenity-rs/songbird/commit/b39ab982238a50eecfefbfd25a07ffc1be44a516
[c:b46a568]: https://github.com/serenity-rs/songbird/commit/b46a568fb51b4ae92841485ee626728d43ad0731
[c:1c52e6e]: https://github.com/serenity-rs/songbird/commit/1c52e6ebc047d21a64551704cc02ef42d29f5f86
[c:b435e16]: https://github.com/serenity-rs/songbird/commit/b435e167d2760cd4a353fa422f33a0039f4151b5
[c:9a244ba]: https://github.com/serenity-rs/songbird/commit/9a244ba4c28377458a0ebb039f20093b09969399
[c:e9b2243]: https://github.com/serenity-rs/songbird/commit/e9b2243b83e61c874b0026e8d223ecbb7b879bba
[c:3bc132f]: https://github.com/serenity-rs/songbird/commit/3bc132faaff77bafaae85f93ed6a0a82083b8ec1
[c:c81f2a9]: https://github.com/serenity-rs/songbird/commit/c81f2a95780b0e32b878a664c8e69f714650e47a
[c:c4331c4]: https://github.com/serenity-rs/songbird/commit/c4331c451fc233ac97c8363cea32e6c5b30c5df5
[c:0d6a226]: https://github.com/serenity-rs/songbird/commit/0d6a226910943a56c5d53e3651958f480c5ee7c4
[c:2bcc522]: https://github.com/serenity-rs/songbird/commit/2bcc5223feb1e631cd623c71628010a56aff97bb
[c:743a1d2]: https://github.com/serenity-rs/songbird/commit/743a1d262e6bf9e9f3767b6d9646c680ef4172c4
<!-- generated by git-cliff -->
## [v0.4.6] — 2024-12-04 ## [v0.4.6] — 2024-12-04
This patch release adds the ability to pass custom arguments into `yt-dlp` instances, and fixes early event handling needed for voice receive as well as inner track handle state after `set_track` is called. This patch release adds the ability to pass custom arguments into `yt-dlp` instances, and fixes early event handling needed for voice receive as well as inner track handle state after `set_track` is called.
@@ -23,7 +93,6 @@ Thanks to the following for their contributions:
<!-- COMPARISONS --> <!-- COMPARISONS -->
[v0.4.6]: https://github.com/serenity-rs/songbird/compare/v0.4.5...v0.4.6 [v0.4.6]: https://github.com/serenity-rs/songbird/compare/v0.4.5...v0.4.6
<!-- COMMITS --> <!-- COMMITS -->
[c:651d037]: https://github.com/serenity-rs/songbird/commit/651d037a54ce37f5b69f456ccdfdb03ba9de8a91 [c:651d037]: https://github.com/serenity-rs/songbird/commit/651d037a54ce37f5b69f456ccdfdb03ba9de8a91
[c:17993bc]: https://github.com/serenity-rs/songbird/commit/17993bc0d0ae7a4e43e86bbb6439c30538b67832 [c:17993bc]: https://github.com/serenity-rs/songbird/commit/17993bc0d0ae7a4e43e86bbb6439c30538b67832
@@ -789,7 +858,9 @@ We'd also like to thank all users who have contributed to this module in the pas
[@clarity0]: https://github.com/clarity0 [@clarity0]: https://github.com/clarity0
[@cycle-five]: https://github.com/cycle-five [@cycle-five]: https://github.com/cycle-five
[@DasEtwas]: https://github.com/DasEtwas [@DasEtwas]: https://github.com/DasEtwas
[@decahedron1]: https://github.com/decahedron1
[@DoumanAsh]: https://github.com/DoumanAsh [@DoumanAsh]: https://github.com/DoumanAsh
[@DPlayer234]: https://github.com/DPlayer234
[@Elinvynia]: https://github.com/Elinvynia [@Elinvynia]: https://github.com/Elinvynia
[@Erk-]: https://github.com/Erk- [@Erk-]: https://github.com/Erk-
[@fee1-dead]: https://github.com/fee1-dead [@fee1-dead]: https://github.com/fee1-dead

View File

@@ -10,8 +10,8 @@ license = "ISC"
name = "songbird" name = "songbird"
readme = "README.md" readme = "README.md"
repository = "https://github.com/serenity-rs/songbird.git" repository = "https://github.com/serenity-rs/songbird.git"
rust-version = "1.74" rust-version = "1.74.1"
version = "0.4.6" version = "0.5.0"
[dependencies] [dependencies]
aead = { optional = true, version = "0.5.2" } aead = { optional = true, version = "0.5.2" }
@@ -21,7 +21,7 @@ audiopus = { optional = true, version = "0.3.0-rc.0" }
byteorder = { optional = true, version = "1" } byteorder = { optional = true, version = "1" }
bytes = { optional = true, version = "1" } bytes = { optional = true, version = "1" }
chacha20poly1305 = { optional = true, version = "0.10.1" } chacha20poly1305 = { optional = true, version = "0.10.1" }
crypto_secretbox = { optional = true, features = ["std"], version = "0.1" } crypto-common = { optional = true, features = ["std"], version = "0.1" }
dashmap = { optional = true, version = "6.1.0" } dashmap = { optional = true, version = "6.1.0" }
derivative = "2" derivative = "2"
discortp = { default-features = false, features = [ discortp = { default-features = false, features = [
@@ -99,7 +99,7 @@ driver = [
"dep:byteorder", "dep:byteorder",
"dep:bytes", "dep:bytes",
"dep:chacha20poly1305", "dep:chacha20poly1305",
"dep:crypto_secretbox", "dep:crypto-common",
"dep:discortp", "dep:discortp",
"dep:flume", "dep:flume",
"dep:nohash-hasher", "dep:nohash-hasher",

View File

@@ -30,13 +30,13 @@ project you will need to depend on Symphonia as well.
```toml ```toml
# Including songbird alone gives you support for Opus via the DCA file format. # Including songbird alone gives you support for Opus via the DCA file format.
[dependencies.songbird] [dependencies.songbird]
version = "0.4" version = "0.5"
features = ["builtin-queue"] features = ["builtin-queue"]
# To get additional codecs, you *must* add Symphonia yourself. # To get additional codecs, you *must* add Symphonia yourself.
# This includes the default formats (MKV/WebM, Ogg, Wave) and codecs (FLAC, PCM, Vorbis)... # This includes the default formats (MKV/WebM, Ogg, Wave) and codecs (FLAC, PCM, Vorbis)...
[dependencies.symphonia] [dependencies.symphonia]
version = "0.5.2" version = "0.5"
features = ["aac", "mp3", "isomp4", "alac"] # ...as well as any extras you need! features = ["aac", "mp3", "isomp4", "alac"] # ...as well as any extras you need!
``` ```

View File

@@ -11,7 +11,7 @@ resolver = "2"
[workspace.dependencies] [workspace.dependencies]
reqwest = "0.12" reqwest = "0.12"
serenity = { features = ["cache", "framework", "standard_framework", "voice", "http", "rustls_backend"], version = "0.12" } serenity = { features = ["cache", "framework", "standard_framework", "voice", "http", "rustls_backend"], version = "0.12" }
songbird = { path = "../", version = "0.4", default-features = false } songbird = { path = "../", version = "0.5", default-features = false }
symphonia = { features = ["aac", "mp3", "isomp4", "alac"], version = "0.5.2" } symphonia = { features = ["aac", "mp3", "isomp4", "alac"], version = "0.5.2" }
tokio = { features = ["macros", "rt-multi-thread", "signal", "sync"], version = "1" } tokio = { features = ["macros", "rt-multi-thread", "signal", "sync"], version = "1" }
tracing = "0.1" tracing = "0.1"

View File

@@ -8,8 +8,9 @@ edition = "2021"
futures = "0.3" futures = "0.3"
reqwest = { workspace = true } reqwest = { workspace = true }
# In an actual twilight project, use the "tws" feature as below. # In an actual twilight project, use the "tws" feature as below.
# Tungstenite is used here due to workspace feature unification.
# songbird = { workspace = true, features = ["driver", "gateway", "twilight", "rustls", "tws"] } # songbird = { workspace = true, features = ["driver", "gateway", "twilight", "rustls", "tws"] }
songbird = { workspace = true, features = ["driver", "gateway", "twilight", "rustls"] } songbird = { workspace = true, features = ["driver", "gateway", "twilight", "rustls", "tungstenite"] }
symphonia = { workspace = true } symphonia = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true, default-features = true } tracing-subscriber = { workspace = true, default-features = true }

View File

@@ -4,7 +4,7 @@ use crate::{
driver::tasks::{error::Recipient, message::*}, driver::tasks::{error::Recipient, message::*},
ws::Error as WsError, ws::Error as WsError,
}; };
use crypto_secretbox::Error as CryptoError; use aes_gcm::Error as CryptoError;
use flume::SendError; use flume::SendError;
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
use std::{error::Error as StdError, fmt, io::Error as IoError}; use std::{error::Error as StdError, fmt, io::Error as IoError};
@@ -118,6 +118,7 @@ impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self { match self {
Error::AttemptDiscarded Error::AttemptDiscarded
| Error::Crypto(_)
| Error::CryptoInvalidLength | Error::CryptoInvalidLength
| Error::CryptoModeInvalid | Error::CryptoModeInvalid
| Error::CryptoModeUnavailable | Error::CryptoModeUnavailable
@@ -127,7 +128,6 @@ impl StdError for Error {
| Error::InterconnectFailure(_) | Error::InterconnectFailure(_)
| Error::Ws(_) | Error::Ws(_)
| Error::TimedOut => None, | Error::TimedOut => None,
Error::Crypto(e) => e.source(),
Error::Io(e) => e.source(), Error::Io(e) => e.source(),
Error::Json(e) => e.source(), Error::Json(e) => e.source(),
} }

View File

@@ -53,12 +53,12 @@ impl Connection {
} }
pub(crate) async fn new_inner( pub(crate) async fn new_inner(
mut info: ConnectionInfo, info: ConnectionInfo,
interconnect: &Interconnect, interconnect: &Interconnect,
config: &Config, config: &Config,
idx: usize, idx: usize,
) -> Result<Connection> { ) -> Result<Connection> {
let url = generate_url(&mut info.endpoint)?; let url = generate_url(&info.endpoint)?;
let mut client = WsStream::connect(url).await?; let mut client = WsStream::connect(url).await?;
let (ws_msg_tx, ws_msg_rx) = flume::unbounded(); let (ws_msg_tx, ws_msg_rx) = flume::unbounded();
@@ -272,7 +272,7 @@ impl Connection {
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn reconnect_inner(&mut self) -> Result<()> { pub async fn reconnect_inner(&mut self) -> Result<()> {
let url = generate_url(&mut self.info.endpoint)?; let url = generate_url(&self.info.endpoint)?;
// Thread may have died, we want to send to prompt a clean exit // Thread may have died, we want to send to prompt a clean exit
// (if at all possible) and then proceed as normal. // (if at all possible) and then proceed as normal.
@@ -331,13 +331,7 @@ impl Drop for Connection {
} }
} }
fn generate_url(endpoint: &mut String) -> Result<Url> { fn generate_url(endpoint: &str) -> Result<Url> {
if endpoint.ends_with(":80") {
let len = endpoint.len();
endpoint.truncate(len - 3);
}
Url::parse(&format!("wss://{endpoint}/?v={VOICE_GATEWAY_VERSION}")).or(Err(Error::EndpointUrl)) Url::parse(&format!("wss://{endpoint}/?v={VOICE_GATEWAY_VERSION}")).or(Err(Error::EndpointUrl))
} }

View File

@@ -2,25 +2,24 @@
#[cfg(any(feature = "receive", test))] #[cfg(any(feature = "receive", test))]
use super::tasks::error::Error as InternalError; use super::tasks::error::Error as InternalError;
use aead::AeadCore; use aead::AeadCore;
use aes_gcm::{AeadInPlace, Aes256Gcm, KeyInit}; use aes_gcm::{AeadInPlace, Aes256Gcm, Error as CryptoError};
use byteorder::{NetworkEndian, WriteBytesExt}; use byteorder::{NetworkEndian, WriteBytesExt};
use chacha20poly1305::XChaCha20Poly1305; use chacha20poly1305::XChaCha20Poly1305;
use crypto_secretbox::{cipher::InvalidLength, Error as CryptoError, XSalsa20Poly1305}; use crypto_common::{InvalidLength, KeyInit};
#[cfg(feature = "receive")] #[cfg(feature = "receive")]
use discortp::rtcp::MutableRtcpPacket; use discortp::rtcp::MutableRtcpPacket;
use discortp::{rtp::RtpPacket, MutablePacket}; use discortp::MutablePacket;
#[cfg(any(feature = "receive", test))] #[cfg(any(feature = "receive", test))]
use discortp::{ use discortp::{
rtp::{MutableRtpPacket, RtpExtensionPacket}, rtp::{MutableRtpPacket, RtpExtensionPacket},
Packet, Packet,
}; };
use rand::Rng;
use std::{num::Wrapping, str::FromStr}; use std::{num::Wrapping, str::FromStr};
use typenum::Unsigned; use typenum::Unsigned;
use crate::error::ConnectionError; use crate::error::ConnectionError;
/// Variants of the `XSalsa20Poly1305` encryption scheme. /// Encryption schemes supportd by Discord.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Hash)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Default, Hash)]
#[non_exhaustive] #[non_exhaustive]
pub enum CryptoMode { pub enum CryptoMode {
@@ -47,45 +46,11 @@ pub enum CryptoMode {
/// ///
/// Nonce width of 4B (32b), at an extra 4B per packet (~0.2 kB/s). /// Nonce width of 4B (32b), at an extra 4B per packet (~0.2 kB/s).
XChaCha20Poly1305, XChaCha20Poly1305,
#[deprecated(
since = "0.4.4",
note = "This voice encryption mode will no longer be accepted by Discord\
as of 2024-11-18. This variant will be removed in `v0.5`."
)]
/// The RTP header is used as the source of nonce bytes for the packet.
///
/// Equivalent to a nonce of at most 48b (6B) at no extra packet overhead:
/// the RTP sequence number and timestamp are the varying quantities.
Normal,
#[deprecated(
since = "0.4.4",
note = "This voice encryption mode will no longer be accepted by Discord\
as of 2024-11-18. This variant will be removed in `v0.5`."
)]
/// An additional random 24B suffix is used as the source of nonce bytes for the packet.
/// This is regenerated randomly for each packet.
///
/// Full nonce width of 24B (192b), at an extra 24B per packet (~1.2 kB/s).
Suffix,
#[deprecated(
since = "0.4.4",
note = "This voice encryption mode will no longer be accepted by Discord\
as of 2024-11-18. This variant will be removed in `v0.5`."
)]
/// An additional random 4B suffix is used as the source of nonce bytes for the packet.
/// This nonce value increments by `1` with each packet.
///
/// Nonce width of 4B (32b), at an extra 4B per packet (~0.2 kB/s).
Lite,
} }
#[allow(deprecated)]
impl From<CryptoState> for CryptoMode { impl From<CryptoState> for CryptoMode {
fn from(val: CryptoState) -> Self { fn from(val: CryptoState) -> Self {
match val { match val {
CryptoState::Normal => Self::Normal,
CryptoState::Suffix => Self::Suffix,
CryptoState::Lite(_) => Self::Lite,
CryptoState::Aes256Gcm(_) => Self::Aes256Gcm, CryptoState::Aes256Gcm(_) => Self::Aes256Gcm,
CryptoState::XChaCha20Poly1305(_) => Self::XChaCha20Poly1305, CryptoState::XChaCha20Poly1305(_) => Self::XChaCha20Poly1305,
} }
@@ -99,20 +64,15 @@ pub struct UnrecognisedCryptoMode;
impl FromStr for CryptoMode { impl FromStr for CryptoMode {
type Err = UnrecognisedCryptoMode; type Err = UnrecognisedCryptoMode;
#[allow(deprecated)]
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"aead_aes256_gcm_rtpsize" => Ok(Self::Aes256Gcm), "aead_aes256_gcm_rtpsize" => Ok(Self::Aes256Gcm),
"aead_xchacha20_poly1305_rtpsize" => Ok(Self::XChaCha20Poly1305), "aead_xchacha20_poly1305_rtpsize" => Ok(Self::XChaCha20Poly1305),
"xsalsa20_poly1305" => Ok(Self::Normal),
"xsalsa20_poly1305_suffix" => Ok(Self::Suffix),
"xsalsa20_poly1305_lite" => Ok(Self::Lite),
_ => Err(UnrecognisedCryptoMode), _ => Err(UnrecognisedCryptoMode),
} }
} }
} }
#[allow(deprecated)]
impl CryptoMode { impl CryptoMode {
/// Returns the underlying crypto algorithm used by a given [`CryptoMode`]. /// Returns the underlying crypto algorithm used by a given [`CryptoMode`].
#[must_use] #[must_use]
@@ -120,22 +80,6 @@ impl CryptoMode {
match self { match self {
CryptoMode::Aes256Gcm => EncryptionAlgorithm::Aes256Gcm, CryptoMode::Aes256Gcm => EncryptionAlgorithm::Aes256Gcm,
CryptoMode::XChaCha20Poly1305 => EncryptionAlgorithm::XChaCha20Poly1305, CryptoMode::XChaCha20Poly1305 => EncryptionAlgorithm::XChaCha20Poly1305,
CryptoMode::Normal | CryptoMode::Suffix | CryptoMode::Lite =>
EncryptionAlgorithm::XSalsa20Poly1305,
}
}
/// Returns whether this [`CryptoMode`] dynamically sizes the ciphertext region
/// to begin in the middle of RTP extensions.
///
/// Compliant SRTP would leave all extensions in cleartext, hence 'more' SRTP
/// compliant.
#[must_use]
#[cfg(any(feature = "receive", test))]
pub(crate) const fn is_more_srtp_compliant(self) -> bool {
match self {
CryptoMode::Aes256Gcm | CryptoMode::XChaCha20Poly1305 => true,
CryptoMode::Normal | CryptoMode::Suffix | CryptoMode::Lite => false,
} }
} }
@@ -149,8 +93,6 @@ impl CryptoMode {
.map(Cipher::Aes256Gcm), .map(Cipher::Aes256Gcm),
EncryptionAlgorithm::XChaCha20Poly1305 => EncryptionAlgorithm::XChaCha20Poly1305 =>
XChaCha20Poly1305::new_from_slice(key).map(Cipher::XChaCha20Poly1305), XChaCha20Poly1305::new_from_slice(key).map(Cipher::XChaCha20Poly1305),
EncryptionAlgorithm::XSalsa20Poly1305 =>
XSalsa20Poly1305::new_from_slice(key).map(|v| Cipher::XSalsa20Poly1305(v, self)),
} }
} }
@@ -160,11 +102,8 @@ impl CryptoMode {
#[must_use] #[must_use]
pub(crate) fn priority(self) -> u64 { pub(crate) fn priority(self) -> u64 {
match self { match self {
CryptoMode::Aes256Gcm => 4, CryptoMode::Aes256Gcm => 1,
CryptoMode::XChaCha20Poly1305 => 3, CryptoMode::XChaCha20Poly1305 => 0,
CryptoMode::Normal => 2,
CryptoMode::Suffix => 1,
CryptoMode::Lite => 0,
} }
} }
@@ -213,9 +152,6 @@ impl CryptoMode {
#[must_use] #[must_use]
pub const fn to_request_str(self) -> &'static str { pub const fn to_request_str(self) -> &'static str {
match self { match self {
Self::Normal => "xsalsa20_poly1305",
Self::Suffix => "xsalsa20_poly1305_suffix",
Self::Lite => "xsalsa20_poly1305_lite",
Self::Aes256Gcm => "aead_aes256_gcm_rtpsize", Self::Aes256Gcm => "aead_aes256_gcm_rtpsize",
Self::XChaCha20Poly1305 => "aead_xchacha20_poly1305_rtpsize", Self::XChaCha20Poly1305 => "aead_xchacha20_poly1305_rtpsize",
} }
@@ -226,7 +162,6 @@ impl CryptoMode {
pub const fn algorithm_nonce_size(self) -> usize { pub const fn algorithm_nonce_size(self) -> usize {
use typenum::Unsigned as _; use typenum::Unsigned as _;
match self { match self {
Self::Lite | Self::Normal | Self::Suffix => XSalsa20Poly1305::NONCE_SIZE,
Self::XChaCha20Poly1305 => <XChaCha20Poly1305 as AeadCore>::NonceSize::USIZE, // => 24 Self::XChaCha20Poly1305 => <XChaCha20Poly1305 as AeadCore>::NonceSize::USIZE, // => 24
Self::Aes256Gcm => <Aes256Gcm as AeadCore>::NonceSize::USIZE, // => 12 Self::Aes256Gcm => <Aes256Gcm as AeadCore>::NonceSize::USIZE, // => 12
} }
@@ -237,35 +172,18 @@ impl CryptoMode {
#[must_use] #[must_use]
pub const fn nonce_size(self) -> usize { pub const fn nonce_size(self) -> usize {
match self { match self {
Self::Aes256Gcm | Self::XChaCha20Poly1305 | Self::Lite => 4, Self::Aes256Gcm | Self::XChaCha20Poly1305 => 4,
Self::Normal => RtpPacket::minimum_packet_size(),
Self::Suffix => XSalsa20Poly1305::NONCE_SIZE,
} }
} }
/// Returns the number of bytes occupied by the XSalsa20Poly1305
/// encryption schemes which fall before the payload.
#[must_use]
#[deprecated(
since = "0.4.4",
note = "This method returns the fixed payload prefix for older encryption modes,\
which will no longer be accepted by Discord as of 2024-11-18. It is an\
implementation detail and will be removed in `v0.5`."
)]
pub fn payload_prefix_len() -> usize {
XSalsa20Poly1305::TAG_SIZE
}
/// Returns the number of bytes occupied by the encryption scheme /// Returns the number of bytes occupied by the encryption scheme
/// which fall before the payload. /// which fall before the payload.
/// ///
/// Method name duplicated until v0.5, to prevent breaking change. /// Method name duplicated until v0.5, to prevent breaking change.
#[must_use] #[must_use]
pub(crate) const fn payload_prefix_len2(self) -> usize { pub(crate) const fn payload_prefix_len(self) -> usize {
match self { match self {
CryptoMode::Aes256Gcm | CryptoMode::XChaCha20Poly1305 => 0, CryptoMode::Aes256Gcm | CryptoMode::XChaCha20Poly1305 => 0,
CryptoMode::Normal | CryptoMode::Suffix | CryptoMode::Lite =>
XSalsa20Poly1305::TAG_SIZE,
} }
} }
@@ -279,41 +197,32 @@ impl CryptoMode {
/// which fall after the payload. /// which fall after the payload.
#[must_use] #[must_use]
pub const fn payload_suffix_len(self) -> usize { pub const fn payload_suffix_len(self) -> usize {
match self { self.nonce_size() + self.encryption_tag_len()
Self::Normal => 0,
Self::Suffix | Self::Lite => self.nonce_size(),
Self::Aes256Gcm | Self::XChaCha20Poly1305 =>
self.nonce_size() + self.encryption_tag_len(),
}
} }
/// Returns the number of bytes occupied by an encryption scheme's tag which /// Returns the number of bytes occupied by an encryption scheme's tag which
/// fall *after* the payload. /// fall *after* the payload.
#[must_use] #[must_use]
pub const fn tag_suffix_len(self) -> usize { pub const fn tag_suffix_len(self) -> usize {
match self { self.encryption_tag_len()
Self::Normal | Self::Suffix | Self::Lite => 0,
Self::Aes256Gcm | Self::XChaCha20Poly1305 => self.encryption_tag_len(),
}
} }
/// Calculates the number of additional bytes required compared /// Calculates the number of additional bytes required compared
/// to an unencrypted payload. /// to an unencrypted payload.
#[must_use] #[must_use]
pub const fn payload_overhead(self) -> usize { pub const fn payload_overhead(self) -> usize {
self.payload_prefix_len2() + self.payload_suffix_len() self.payload_prefix_len() + self.payload_suffix_len()
} }
/// Extracts the byte slice in a packet used as the nonce, and the remaining mutable /// Extracts the byte slice in a packet used as the nonce, and the remaining mutable
/// portion of the packet. /// portion of the packet.
fn nonce_slice<'a>( fn nonce_slice<'a>(
self, self,
header: &'a [u8], _header: &'a [u8],
body: &'a mut [u8], body: &'a mut [u8],
) -> Result<(&'a [u8], &'a mut [u8]), CryptoError> { ) -> Result<(&'a [u8], &'a mut [u8]), CryptoError> {
match self { match self {
Self::Normal => Ok((header, body)), Self::Aes256Gcm | Self::XChaCha20Poly1305 => {
Self::Suffix | Self::Lite | Self::Aes256Gcm | Self::XChaCha20Poly1305 => {
let len = body.len(); let len = body.len();
if len < self.payload_suffix_len() { if len < self.payload_suffix_len() {
Err(CryptoError) Err(CryptoError)
@@ -324,47 +233,6 @@ impl CryptoMode {
}, },
} }
} }
/// Encrypts a Discord RT(C)P packet using the given XSalsa20Poly1305 cipher.
///
/// Use of this requires that the input packet has had a nonce generated in the correct location,
/// and `payload_len` specifies the number of bytes after the header including this nonce.
#[deprecated(
since = "0.4.4",
note = "This method performs encryption for older encryption modes,\
which will no longer be accepted by Discord as of 2024-11-18. It is an\
implementation detail and will be removed in `v0.5`."
)]
#[inline]
pub fn encrypt_in_place(
self,
packet: &mut impl MutablePacket,
cipher: &XSalsa20Poly1305,
payload_len: usize,
) -> Result<(), CryptoError> {
let header_len = packet.packet().len() - packet.payload().len();
let (header, body) = packet.packet_mut().split_at_mut(header_len);
let (slice_to_use, body_remaining) = self.nonce_slice(header, &mut body[..payload_len])?;
let nonce_size = self.nonce_size();
let tag_size = self.encryption_tag_len();
let mut nonce = crypto_secretbox::Nonce::default();
let nonce_slice = if slice_to_use.len() == nonce_size {
crypto_secretbox::Nonce::from_slice(&slice_to_use[..nonce_size])
} else {
nonce[..slice_to_use.len()].copy_from_slice(slice_to_use);
&nonce
};
// body_remaining is now correctly truncated by this point.
// the true_payload to encrypt follows after the first TAG_LEN bytes.
let tag =
cipher.encrypt_in_place_detached(nonce_slice, b"", &mut body_remaining[tag_size..])?;
body_remaining[..tag_size].copy_from_slice(&tag[..]);
Ok(())
}
} }
/// State used in nonce generation for the encryption variants in [`CryptoMode`]. /// State used in nonce generation for the encryption variants in [`CryptoMode`].
@@ -381,29 +249,11 @@ pub enum CryptoState {
/// ///
/// The last used nonce is stored. /// The last used nonce is stored.
XChaCha20Poly1305(Wrapping<u32>), XChaCha20Poly1305(Wrapping<u32>),
/// The RTP header is used as the source of nonce bytes for the packet.
///
/// No state is required.
Normal,
/// An additional random 24B suffix is used as the source of nonce bytes for the packet.
/// This is regenerated randomly for each packet.
///
/// No state is required.
Suffix,
/// An additional random 4B suffix is used as the source of nonce bytes for the packet.
/// This nonce value increments by `1` with each packet.
///
/// The last used nonce is stored.
Lite(Wrapping<u32>),
} }
#[allow(deprecated)]
impl From<CryptoMode> for CryptoState { impl From<CryptoMode> for CryptoState {
fn from(val: CryptoMode) -> Self { fn from(val: CryptoMode) -> Self {
match val { match val {
CryptoMode::Normal => CryptoState::Normal,
CryptoMode::Suffix => CryptoState::Suffix,
CryptoMode::Lite => CryptoState::Lite(Wrapping(rand::random::<u32>())),
CryptoMode::Aes256Gcm => CryptoState::Aes256Gcm(Wrapping(rand::random::<u32>())), CryptoMode::Aes256Gcm => CryptoState::Aes256Gcm(Wrapping(rand::random::<u32>())),
CryptoMode::XChaCha20Poly1305 => CryptoMode::XChaCha20Poly1305 =>
CryptoState::XChaCha20Poly1305(Wrapping(rand::random::<u32>())), CryptoState::XChaCha20Poly1305(Wrapping(rand::random::<u32>())),
@@ -423,12 +273,7 @@ impl CryptoState {
let startpoint = endpoint - mode.nonce_size(); let startpoint = endpoint - mode.nonce_size();
match self { match self {
Self::Suffix => { Self::Aes256Gcm(ref mut i) | Self::XChaCha20Poly1305(ref mut i) => {
rand::rng().fill(&mut packet.payload_mut()[startpoint..endpoint]);
},
Self::Lite(ref mut i)
| Self::Aes256Gcm(ref mut i)
| Self::XChaCha20Poly1305(ref mut i) => {
(&mut packet.payload_mut()[startpoint..endpoint]) (&mut packet.payload_mut()[startpoint..endpoint])
.write_u32::<NetworkEndian>(i.0) .write_u32::<NetworkEndian>(i.0)
.expect( .expect(
@@ -436,7 +281,6 @@ impl CryptoState {
); );
*i += Wrapping(1); *i += Wrapping(1);
}, },
Self::Normal => {},
} }
endpoint endpoint
@@ -453,7 +297,6 @@ impl CryptoState {
pub(crate) enum EncryptionAlgorithm { pub(crate) enum EncryptionAlgorithm {
Aes256Gcm, Aes256Gcm,
XChaCha20Poly1305, XChaCha20Poly1305,
XSalsa20Poly1305,
} }
impl EncryptionAlgorithm { impl EncryptionAlgorithm {
@@ -462,7 +305,6 @@ impl EncryptionAlgorithm {
match self { match self {
Self::Aes256Gcm => <Aes256Gcm as AeadCore>::TagSize::USIZE, // 16 Self::Aes256Gcm => <Aes256Gcm as AeadCore>::TagSize::USIZE, // 16
Self::XChaCha20Poly1305 => <XChaCha20Poly1305 as AeadCore>::TagSize::USIZE, // 16 Self::XChaCha20Poly1305 => <XChaCha20Poly1305 as AeadCore>::TagSize::USIZE, // 16
Self::XSalsa20Poly1305 => XSalsa20Poly1305::TAG_SIZE, // 16
} }
} }
} }
@@ -470,7 +312,6 @@ impl EncryptionAlgorithm {
impl From<&Cipher> for EncryptionAlgorithm { impl From<&Cipher> for EncryptionAlgorithm {
fn from(value: &Cipher) -> Self { fn from(value: &Cipher) -> Self {
match value { match value {
Cipher::XSalsa20Poly1305(..) => EncryptionAlgorithm::XSalsa20Poly1305,
Cipher::XChaCha20Poly1305(_) => EncryptionAlgorithm::XChaCha20Poly1305, Cipher::XChaCha20Poly1305(_) => EncryptionAlgorithm::XChaCha20Poly1305,
Cipher::Aes256Gcm(_) => EncryptionAlgorithm::Aes256Gcm, Cipher::Aes256Gcm(_) => EncryptionAlgorithm::Aes256Gcm,
} }
@@ -479,7 +320,6 @@ impl From<&Cipher> for EncryptionAlgorithm {
#[derive(Clone)] #[derive(Clone)]
pub enum Cipher { pub enum Cipher {
XSalsa20Poly1305(XSalsa20Poly1305, CryptoMode),
XChaCha20Poly1305(XChaCha20Poly1305), XChaCha20Poly1305(XChaCha20Poly1305),
Aes256Gcm(Box<Aes256Gcm>), Aes256Gcm(Box<Aes256Gcm>),
} }
@@ -488,7 +328,6 @@ impl Cipher {
#[must_use] #[must_use]
pub(crate) fn mode(&self) -> CryptoMode { pub(crate) fn mode(&self) -> CryptoMode {
match self { match self {
Cipher::XSalsa20Poly1305(_, mode) => *mode,
Cipher::XChaCha20Poly1305(_) => CryptoMode::XChaCha20Poly1305, Cipher::XChaCha20Poly1305(_) => CryptoMode::XChaCha20Poly1305,
Cipher::Aes256Gcm(_) => CryptoMode::Aes256Gcm, Cipher::Aes256Gcm(_) => CryptoMode::Aes256Gcm,
} }
@@ -519,23 +358,13 @@ impl Cipher {
// body_remaining is now correctly truncated to exclude the nonce by this point. // body_remaining is now correctly truncated to exclude the nonce by this point.
// the true_payload to encrypt is within the buf[prefix:-suffix]. // the true_payload to encrypt is within the buf[prefix:-suffix].
let (pre_payload, body_remaining) = body_remaining.split_at_mut(mode.payload_prefix_len2()); let (_, body_remaining) = body_remaining.split_at_mut(mode.payload_prefix_len());
let (body, post_payload) = let (body, post_payload) =
body_remaining.split_at_mut(body_remaining.len() - mode.tag_suffix_len()); body_remaining.split_at_mut(body_remaining.len() - mode.tag_suffix_len());
// All these Nonce types are distinct at the type level // All these Nonce types are distinct at the type level
// (96b for AES, 192b for XSalsa/XChaCha). // (96b for AES, 192b for XChaCha).
match self { match self {
// Older modes place the tag before the payload and do not authenticate
// cleartext.
Self::XSalsa20Poly1305(secret_box, _) => {
let mut nonce = crypto_secretbox::Nonce::default();
nonce[..mode.nonce_size()].copy_from_slice(slice_to_use);
let tag = secret_box.encrypt_in_place_detached(&nonce, b"", body)?;
pre_payload[..tag_size].copy_from_slice(&tag[..]);
},
// The below variants follow part of the SRTP spec (RFC3711, sec 3.1) // The below variants follow part of the SRTP spec (RFC3711, sec 3.1)
// by requiring that we include the cleartext header portion as // by requiring that we include the cleartext header portion as
// authenticated data. // authenticated data.
@@ -563,14 +392,13 @@ impl Cipher {
&self, &self,
packet: &mut MutableRtpPacket<'_>, packet: &mut MutableRtpPacket<'_>,
) -> Result<(usize, usize), InternalError> { ) -> Result<(usize, usize), InternalError> {
let mode = self.mode();
// An exciting difference from the SRTP spec: Discord begins encryption // An exciting difference from the SRTP spec: Discord begins encryption
// after the RTP extension *header*, encrypting the extensions themselves, // after the RTP extension *header*, encrypting the extensions themselves,
// whereas the spec leaves all extensions in the clear. // whereas the spec leaves all extensions in the clear.
// This header is described as the 'extension preamble'. // This header is described as the 'extension preamble'.
let has_extension = packet.get_extension() != 0; let has_extension = packet.get_extension() != 0;
let plain_bytes = if mode.is_more_srtp_compliant() && has_extension { let plain_bytes = if has_extension {
// CSRCs and extension bytes will be in the plaintext segment. // CSRCs and extension bytes will be in the plaintext segment.
// We will need these demarcated to select the right bytes to // We will need these demarcated to select the right bytes to
// decrypt, and to use as auth data. // decrypt, and to use as auth data.
@@ -579,19 +407,19 @@ impl Cipher {
0 0
}; };
let (mut start_estimate, end) = self.decrypt_pkt_in_place(packet, plain_bytes)?; let (_, end) = self.decrypt_pkt_in_place(packet, plain_bytes)?;
// Update the start estimate to account for bytes occupied by extension headers. // Update the start estimate to account for bytes occupied by extension headers.
if has_extension { let payload_offset = if has_extension {
let packet = packet.packet(); let payload = packet.payload();
if let Some((_, exts_and_opus)) = split_at_checked(packet, start_estimate) { let extension =
let extension = RtpExtensionPacket::new(exts_and_opus) RtpExtensionPacket::new(payload).ok_or(InternalError::IllegalVoicePacket)?;
.ok_or(InternalError::IllegalVoicePacket)?; extension.packet().len() - extension.payload().len()
start_estimate += extension.packet().len() - extension.payload().len(); } else {
} 0
} };
Ok((start_estimate, end)) Ok((payload_offset, end))
} }
#[cfg(feature = "receive")] #[cfg(feature = "receive")]
@@ -624,7 +452,7 @@ impl Cipher {
let (slice_to_use, body_remaining) = mode.nonce_slice(plaintext, ciphertext)?; let (slice_to_use, body_remaining) = mode.nonce_slice(plaintext, ciphertext)?;
let (pre_payload, body_remaining) = let (pre_payload, body_remaining) =
split_at_mut_checked(body_remaining, mode.payload_prefix_len2()).ok_or(CryptoError)?; split_at_mut_checked(body_remaining, mode.payload_prefix_len()).ok_or(CryptoError)?;
let suffix_split_point = body_remaining let suffix_split_point = body_remaining
.len() .len()
@@ -637,16 +465,6 @@ impl Cipher {
let tag_size = self.encryption_tag_len(); let tag_size = self.encryption_tag_len();
match self { match self {
// Older modes place the tag before the payload and do not authenticate
// cleartext.
Self::XSalsa20Poly1305(secret_box, _) => {
let mut nonce = crypto_secretbox::Nonce::default();
nonce[..mode.nonce_size().min(slice_to_use.len())].copy_from_slice(slice_to_use);
let tag = crypto_secretbox::Tag::from_slice(&pre_payload[..tag_size]);
secret_box.decrypt_in_place_detached(&nonce, b"", body, tag)?;
},
// The below variants follow part of the SRTP spec (RFC3711, sec 3.1) // The below variants follow part of the SRTP spec (RFC3711, sec 3.1)
// by requiring that we include the cleartext header portion as // by requiring that we include the cleartext header portion as
// authenticated data. // authenticated data.
@@ -666,23 +484,15 @@ impl Cipher {
}, },
} }
Ok((plaintext_end + pre_payload.len(), post_payload.len())) Ok((
plaintext_end + pre_payload.len(),
post_payload.len() + slice_to_use.len(),
))
} }
} }
// Temporary functions -- MSRV is ostensibly 1.74, slice::split_at(_mut)_checked is 1.80+. // Temporary functions -- MSRV is ostensibly 1.74, slice::split_at(_mut)_checked is 1.80+.
// TODO: Remove in v0.5+ with MSRV bump to 1.81+. // TODO: Remove in v0.5+ with MSRV bump to 1.81+.
#[cfg(any(feature = "receive", test))]
#[inline]
#[must_use]
const fn split_at_checked(els: &[u8], mid: usize) -> Option<(&[u8], &[u8])> {
if mid <= els.len() {
Some(els.split_at(mid))
} else {
None
}
}
#[cfg(any(feature = "receive", test))] #[cfg(any(feature = "receive", test))]
#[inline] #[inline]
#[must_use] #[must_use]
@@ -700,16 +510,10 @@ mod test {
use discortp::rtp::MutableRtpPacket; use discortp::rtp::MutableRtpPacket;
#[test] #[test]
#[allow(deprecated)]
fn small_packet_decrypts_error() { fn small_packet_decrypts_error() {
let mut buf = [0u8; MutableRtpPacket::minimum_packet_size()]; let mut buf = [0u8; MutableRtpPacket::minimum_packet_size()];
let modes = [ let modes = [CryptoMode::Aes256Gcm, CryptoMode::XChaCha20Poly1305];
CryptoMode::Normal,
CryptoMode::Suffix,
CryptoMode::Lite,
CryptoMode::Aes256Gcm,
CryptoMode::XChaCha20Poly1305,
];
let mut pkt = MutableRtpPacket::new(&mut buf[..]).unwrap(); let mut pkt = MutableRtpPacket::new(&mut buf[..]).unwrap();
for mode in modes { for mode in modes {
@@ -720,41 +524,6 @@ mod test {
} }
} }
#[test]
#[allow(deprecated)]
fn symmetric_encrypt_decrypt_xsalsa20() {
const TRUE_PAYLOAD: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
let mut buf = [0u8; MutableRtpPacket::minimum_packet_size()
+ TRUE_PAYLOAD.len()
+ XSalsa20Poly1305::TAG_SIZE
+ XSalsa20Poly1305::NONCE_SIZE];
let modes = [CryptoMode::Normal, CryptoMode::Lite, CryptoMode::Suffix];
for mode in modes {
buf.fill(0);
let cipher = mode
.cipher_from_key(&[7u8; XSalsa20Poly1305::KEY_SIZE])
.unwrap();
let mut pkt = MutableRtpPacket::new(&mut buf[..]).unwrap();
let mut crypto_state = CryptoState::from(mode);
let payload = pkt.payload_mut();
payload[XSalsa20Poly1305::TAG_SIZE..XSalsa20Poly1305::TAG_SIZE + TRUE_PAYLOAD.len()]
.copy_from_slice(&TRUE_PAYLOAD[..]);
let final_payload_size = crypto_state
.write_packet_nonce(&mut pkt, XSalsa20Poly1305::TAG_SIZE + TRUE_PAYLOAD.len());
let enc_succ = cipher.encrypt_pkt_in_place(&mut pkt, final_payload_size);
assert!(enc_succ.is_ok());
let final_pkt_len = MutableRtpPacket::minimum_packet_size() + final_payload_size;
let mut pkt = MutableRtpPacket::new(&mut buf[..final_pkt_len]).unwrap();
assert!(cipher.decrypt_rtp_in_place(&mut pkt).is_ok());
}
}
#[test] #[test]
fn symmetric_encrypt_decrypt_tag_after_data() { fn symmetric_encrypt_decrypt_tag_after_data() {
const TRUE_PAYLOAD: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; const TRUE_PAYLOAD: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8];
@@ -772,7 +541,7 @@ mod test {
let mut pkt = MutableRtpPacket::new(&mut buf[..]).unwrap(); let mut pkt = MutableRtpPacket::new(&mut buf[..]).unwrap();
let mut crypto_state = CryptoState::from(mode); let mut crypto_state = CryptoState::from(mode);
let payload = pkt.payload_mut(); let payload = pkt.payload_mut();
payload[mode.payload_prefix_len2()..TRUE_PAYLOAD.len()].copy_from_slice(&TRUE_PAYLOAD); payload[mode.payload_prefix_len()..TRUE_PAYLOAD.len()].copy_from_slice(&TRUE_PAYLOAD);
let final_payload_size = crypto_state.write_packet_nonce(&mut pkt, TRUE_PAYLOAD.len()); let final_payload_size = crypto_state.write_packet_nonce(&mut pkt, TRUE_PAYLOAD.len());
@@ -788,33 +557,31 @@ mod test {
} }
#[test] #[test]
#[allow(deprecated)]
fn negotiate_cryptomode() { fn negotiate_cryptomode() {
// If we have no preference (or our preference is missing), choose the highest available in the set. // If we have no preference (or our preference is missing), choose the highest available in the set.
let test_set = [ let test_set =
CryptoMode::Suffix, [CryptoMode::XChaCha20Poly1305, CryptoMode::Aes256Gcm].map(CryptoMode::to_request_str);
CryptoMode::XChaCha20Poly1305,
CryptoMode::Lite,
]
.map(CryptoMode::to_request_str);
assert_eq!( assert_eq!(
CryptoMode::negotiate(test_set, None).unwrap(), CryptoMode::negotiate(test_set, None).unwrap(),
CryptoMode::XChaCha20Poly1305 CryptoMode::Aes256Gcm
); );
let test_set_missing = [CryptoMode::XChaCha20Poly1305].map(CryptoMode::to_request_str);
assert_eq!( assert_eq!(
CryptoMode::negotiate(test_set, Some(CryptoMode::Aes256Gcm)).unwrap(), CryptoMode::negotiate(test_set_missing, Some(CryptoMode::Aes256Gcm)).unwrap(),
CryptoMode::XChaCha20Poly1305 CryptoMode::XChaCha20Poly1305
); );
// Preference wins in spite of the defined `priority` value. // Preference wins in spite of the defined `priority` value.
assert_eq!( assert_eq!(
CryptoMode::negotiate(test_set, Some(CryptoMode::Suffix)).unwrap(), CryptoMode::negotiate(test_set, Some(CryptoMode::XChaCha20Poly1305)).unwrap(),
CryptoMode::Suffix CryptoMode::XChaCha20Poly1305
); );
// If there is no mutual intelligibility, return an error. // If there is no mutual intelligibility, return an error.
let bad_modes = ["not_real", "des", "rc5"]; let bad_modes = ["not_real", "des", "rc5"];
assert!(CryptoMode::negotiate(&bad_modes, None).is_err()); assert!(CryptoMode::negotiate(bad_modes, None).is_err());
assert!(CryptoMode::negotiate(&bad_modes, Some(CryptoMode::Aes256Gcm)).is_err()); assert!(CryptoMode::negotiate(bad_modes, Some(CryptoMode::Aes256Gcm)).is_err());
} }
} }

View File

@@ -1,7 +1,7 @@
use super::message::*; use super::message::*;
use crate::ws::Error as WsError; use crate::ws::Error as WsError;
use aes_gcm::Error as CryptoError;
use audiopus::Error as OpusError; use audiopus::Error as OpusError;
use crypto_secretbox::aead::Error as CryptoError;
use flume::SendError; use flume::SendError;
use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use std::io::{Error as IoError, ErrorKind as IoErrorKind};

View File

@@ -246,7 +246,7 @@ pub fn mix_symph_indiv(
resample_in_progress = true; resample_in_progress = true;
continue; continue;
} }
}; }
let samples_marched = mix_resampled(rs_out_buf, symph_mix, samples_written, volume); let samples_marched = mix_resampled(rs_out_buf, symph_mix, samples_written, volume);

View File

@@ -554,7 +554,7 @@ impl Mixer {
); );
let payload = rtp.payload_mut(); let payload = rtp.payload_mut();
let pre_len = self.crypto_mode().payload_prefix_len2(); let pre_len = self.crypto_mode().payload_prefix_len();
payload[pre_len..pre_len + SILENT_FRAME.len()].copy_from_slice(&SILENT_FRAME[..]); payload[pre_len..pre_len + SILENT_FRAME.len()].copy_from_slice(&SILENT_FRAME[..]);
@@ -591,7 +591,7 @@ impl Mixer {
); );
let payload = rtp.payload(); let payload = rtp.payload();
let opus_frame = let opus_frame =
(payload[self.crypto_mode().payload_prefix_len2()..][..len]).to_vec(); (payload[self.crypto_mode().payload_prefix_len()..][..len]).to_vec();
OutputMessage::Passthrough(opus_frame) OutputMessage::Passthrough(opus_frame)
}, },
@@ -637,7 +637,7 @@ impl Mixer {
let payload = rtp.payload_mut(); let payload = rtp.payload_mut();
let crypto_mode = conn.crypto_state.kind(); let crypto_mode = conn.crypto_state.kind();
let first_payload_byte = crypto_mode.payload_prefix_len2(); let first_payload_byte = crypto_mode.payload_prefix_len();
// If passthrough, Opus payload in place already. // If passthrough, Opus payload in place already.
// Else encode into buffer with space for AEAD encryption headers. // Else encode into buffer with space for AEAD encryption headers.
@@ -679,12 +679,11 @@ impl Mixer {
Ok(()) Ok(())
} else { } else {
self._send_packet(packet) self.send_packet_(packet)
}; };
#[cfg(not(test))] #[cfg(not(test))]
#[allow(clippy::used_underscore_items)] let send_status = self.send_packet_(packet);
let send_status = self._send_packet(packet);
send_status.or_else(Error::disarm_would_block)?; send_status.or_else(Error::disarm_would_block)?;
@@ -692,7 +691,7 @@ impl Mixer {
} }
#[inline] #[inline]
fn _send_packet(&self, packet: &[u8]) -> Result<()> { fn send_packet_(&self, packet: &[u8]) -> Result<()> {
let conn = self let conn = self
.conn_active .conn_active
.as_ref() .as_ref()
@@ -756,7 +755,7 @@ impl Mixer {
(Blame: VOICE_PACKET_MAX?)", (Blame: VOICE_PACKET_MAX?)",
); );
let payload = rtp.payload_mut(); let payload = rtp.payload_mut();
let opus_frame = &mut payload[self.crypto_mode().payload_prefix_len2()..]; let opus_frame = &mut payload[self.crypto_mode().payload_prefix_len()..];
// Opus frame passthrough. // Opus frame passthrough.
// This requires that we have only one PLAYING track, who has volume 1.0, and an // This requires that we have only one PLAYING track, who has volume 1.0, and an

View File

@@ -177,7 +177,7 @@ impl UdpRx {
let rtp = rtp.to_immutable(); let rtp = rtp.to_immutable();
let (rtp_body_start, rtp_body_tail, decrypted) = packet_data.unwrap_or_else(|| { let (rtp_body_start, rtp_body_tail, decrypted) = packet_data.unwrap_or_else(|| {
( (
crypto_mode.payload_prefix_len2(), crypto_mode.payload_prefix_len(),
crypto_mode.payload_suffix_len(), crypto_mode.payload_suffix_len(),
false, false,
) )
@@ -222,7 +222,7 @@ impl UdpRx {
let (start, tail) = packet_data.unwrap_or_else(|| { let (start, tail) = packet_data.unwrap_or_else(|| {
( (
crypto_mode.payload_prefix_len2(), crypto_mode.payload_prefix_len(),
crypto_mode.payload_suffix_len(), crypto_mode.payload_suffix_len(),
) )
}); });

View File

@@ -87,7 +87,7 @@ impl SsrcState {
let extensions = rtp.get_extension() != 0; let extensions = rtp.get_extension() != 0;
let payload = rtp.payload(); let payload = rtp.payload();
let payload_offset = self.crypto_mode.payload_prefix_len2(); let payload_offset = self.crypto_mode.payload_prefix_len();
let payload_end_pad = payload.len() - self.crypto_mode.payload_suffix_len(); let payload_end_pad = payload.len() - self.crypto_mode.payload_suffix_len();
// We still need to compute missed packets here in case of long loss chains or similar. // We still need to compute missed packets here in case of long loss chains or similar.

View File

@@ -15,7 +15,6 @@ use crate::{
test_utils, test_utils,
tracks::LoopState, tracks::LoopState,
}; };
use crypto_secretbox::XSalsa20Poly1305;
use flume::Receiver; use flume::Receiver;
use std::{io::Cursor, net::UdpSocket, sync::Arc}; use std::{io::Cursor, net::UdpSocket, sync::Arc};
use tokio::runtime::Handle; use tokio::runtime::Handle;
@@ -63,11 +62,8 @@ impl Mixer {
.connect("127.0.0.1:5316") .connect("127.0.0.1:5316")
.expect("Failed to connect to local dest port."); .expect("Failed to connect to local dest port.");
#[allow(deprecated)] let mode = CryptoMode::Aes256Gcm;
let mode = CryptoMode::Normal; let cipher = mode.cipher_from_key(&[0u8; 32]).unwrap();
let cipher = mode
.cipher_from_key(&[0u8; XSalsa20Poly1305::KEY_SIZE])
.unwrap();
let crypto_state = mode.into(); let crypto_state = mode.into();
#[cfg(feature = "receive")] #[cfg(feature = "receive")]

View File

@@ -434,7 +434,7 @@ where
}, },
Err(e) => { Err(e) => {
debug!("Read error {:?} {:?} {:?}.", e, out, raw_len); debug!("Read error {:?} {:?} {:?}.", e, out, raw_len);
out = Some(Err(IoError::new(IoErrorKind::Other, e))); out = Some(Err(IoError::other(e)));
break; break;
}, },
} }
@@ -472,7 +472,7 @@ where
// NOTE: use of raw_len here preserves true sample length even if // NOTE: use of raw_len here preserves true sample length even if
// stream is extended to 20ms boundary. // stream is extended to 20ms boundary.
out.unwrap_or_else(|| Err(IoError::new(IoErrorKind::Other, "Unclear."))) out.unwrap_or_else(|| Err(IoError::other("Unclear.")))
.map(|compressed_sz| { .map(|compressed_sz| {
self.audio_bytes self.audio_bytes
.fetch_add(raw_len * mem::size_of::<f32>(), Ordering::Release); .fetch_add(raw_len * mem::size_of::<f32>(), Ordering::Release);

View File

@@ -1,9 +1,12 @@
//! Metadata formats specific to [`crate::input::Compose`] types.
use crate::error::JsonError; use crate::error::JsonError;
use std::time::Duration; use std::time::Duration;
use symphonia_core::{meta::Metadata as ContainerMetadata, probe::ProbedMetadata}; use symphonia_core::{meta::Metadata as ContainerMetadata, probe::ProbedMetadata};
pub(crate) mod ffprobe; pub(crate) mod ffprobe;
pub(crate) mod ytdl; mod ytdl;
pub use ytdl::Output as YoutubeDlOutput;
use super::Parsed; use super::Parsed;

View File

@@ -1,28 +1,49 @@
//! `YoutubeDl` track metadata.
use super::AuxMetadata; use super::AuxMetadata;
use crate::constants::SAMPLE_RATE_RAW; use crate::constants::SAMPLE_RATE_RAW;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, time::Duration};
/// Information returned by yt-dlp about a URL.
///
/// Returned by [`crate::input::YoutubeDl::query`].
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub struct Output { pub struct Output {
/// The main artist.
pub artist: Option<String>, pub artist: Option<String>,
/// The album name.
pub album: Option<String>, pub album: Option<String>,
/// The channel name.
pub channel: Option<String>, pub channel: Option<String>,
/// The duration of the stream in seconds.
pub duration: Option<f64>, pub duration: Option<f64>,
/// The size of the stream.
pub filesize: Option<u64>, pub filesize: Option<u64>,
/// Required HTTP headers to fetch the track stream.
pub http_headers: Option<HashMap<String, String>>, pub http_headers: Option<HashMap<String, String>>,
/// Release date of this track.
pub release_date: Option<String>, pub release_date: Option<String>,
/// The thumbnail URL for this track.
pub thumbnail: Option<String>, pub thumbnail: Option<String>,
/// The title of this track.
pub title: Option<String>, pub title: Option<String>,
/// The track name.
pub track: Option<String>, pub track: Option<String>,
/// The date this track was uploaded on.
pub upload_date: Option<String>, pub upload_date: Option<String>,
/// The name of the uploader.
pub uploader: Option<String>, pub uploader: Option<String>,
/// The stream URL.
pub url: String, pub url: String,
/// The URL of the public-facing webpage for this track.
pub webpage_url: Option<String>, pub webpage_url: Option<String>,
/// The stream protocol.
pub protocol: Option<String>, pub protocol: Option<String>,
} }
impl Output { impl Output {
/// Requests auxiliary metadata which can be accessed without parsing the file.
pub fn as_aux_metadata(&self) -> AuxMetadata { pub fn as_aux_metadata(&self) -> AuxMetadata {
let album = self.album.clone(); let album = self.album.clone();
let track = self.track.clone(); let track = self.track.clone();

View File

@@ -44,7 +44,6 @@
//! [`Read`]: https://doc.rust-lang.org/std/io/trait.Read.html //! [`Read`]: https://doc.rust-lang.org/std/io/trait.Read.html
//! [`Compressed`]: cached::Compressed //! [`Compressed`]: cached::Compressed
//! [DCA1]: https://github.com/bwmarrin/dca //! [DCA1]: https://github.com/bwmarrin/dca
//! [`registry::*`]: registry
//! [`cached::*`]: cached //! [`cached::*`]: cached
//! [`OpusDecoder`]: codecs::OpusDecoder //! [`OpusDecoder`]: codecs::OpusDecoder
//! [`DcaReader`]: codecs::DcaReader //! [`DcaReader`]: codecs::DcaReader
@@ -60,7 +59,7 @@ mod error;
#[cfg(test)] #[cfg(test)]
pub mod input_tests; pub mod input_tests;
mod live_input; mod live_input;
mod metadata; pub mod metadata;
mod parsed; mod parsed;
mod sources; mod sources;
pub mod utils; pub mod utils;
@@ -71,7 +70,7 @@ pub use self::{
compose::*, compose::*,
error::*, error::*,
live_input::*, live_input::*,
metadata::*, metadata::{AuxMetadata, Metadata},
parsed::*, parsed::*,
sources::*, sources::*,
}; };

View File

@@ -132,8 +132,7 @@ impl HttpRequest {
}); });
let stream = Box::new(StreamReader::new( let stream = Box::new(StreamReader::new(
resp.bytes_stream() resp.bytes_stream().map_err(IoError::other),
.map_err(|e| IoError::new(IoErrorKind::Other, e)),
)); ));
let input = HttpStream { let input = HttpStream {

View File

@@ -1,5 +1,5 @@
use crate::input::{ use crate::input::{
metadata::ytdl::Output, metadata::YoutubeDlOutput,
AudioStream, AudioStream,
AudioStreamError, AudioStreamError,
AuxMetadata, AuxMetadata,
@@ -8,7 +8,6 @@ use crate::input::{
Input, Input,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use either::Either;
use reqwest::{ use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue}, header::{HeaderMap, HeaderName, HeaderValue},
Client, Client,
@@ -118,19 +117,19 @@ impl<'a> YoutubeDl<'a> {
) -> Result<impl Iterator<Item = AuxMetadata>, AudioStreamError> { ) -> Result<impl Iterator<Item = AuxMetadata>, AudioStreamError> {
let n_results = n_results.unwrap_or(5); let n_results = n_results.unwrap_or(5);
Ok(match &self.query { Ok(self
// Safer to just return the metadata for the pointee if possible .query(n_results)
QueryType::Url(_) => Either::Left(std::iter::once(self.aux_metadata().await?)), .await?
QueryType::Search(_) => Either::Right( .into_iter()
self.query(n_results) .map(|v| v.as_aux_metadata()))
.await?
.into_iter()
.map(|v| v.as_aux_metadata()),
),
})
} }
async fn query(&mut self, n_results: usize) -> Result<Vec<Output>, AudioStreamError> { /// Runs a search for the given query, returning a list of up to `n_results`
/// possible matches.
pub async fn query(
&mut self,
n_results: usize,
) -> Result<Vec<YoutubeDlOutput>, AudioStreamError> {
let query_str = self.query.as_cow_str(n_results); let query_str = self.query.as_cow_str(n_results);
let ytdl_args = [ let ytdl_args = [
"-j", "-j",
@@ -169,7 +168,7 @@ impl<'a> YoutubeDl<'a> {
.split(|&b| b == b'\n') .split(|&b| b == b'\n')
.filter(|&x| (!x.is_empty())) .filter(|&x| (!x.is_empty()))
.map(serde_json::from_slice) .map(serde_json::from_slice)
.collect::<Result<Vec<Output>, _>>() .collect::<Result<Vec<YoutubeDlOutput>, _>>()
.map_err(|e| AudioStreamError::Fail(Box::new(e)))?; .map_err(|e| AudioStreamError::Fail(Box::new(e)))?;
let meta = out let meta = out
@@ -183,6 +182,41 @@ impl<'a> YoutubeDl<'a> {
Ok(out) Ok(out)
} }
/// Get the audio stream from a [`YoutubeDlOutput`].
pub async fn get_stream(
&self,
result: &YoutubeDlOutput,
) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> {
let mut headers = HeaderMap::default();
if let Some(map) = &result.http_headers {
headers.extend(map.iter().filter_map(|(k, v)| {
Some((
HeaderName::from_bytes(k.as_bytes()).ok()?,
HeaderValue::from_str(v).ok()?,
))
}));
}
#[allow(clippy::single_match_else)]
match result.protocol.as_deref() {
Some("m3u8_native") => {
let mut req =
HlsRequest::new_with_headers(self.client.clone(), result.url.clone(), headers);
req.create()
},
_ => {
let mut req = HttpRequest {
client: self.client.clone(),
request: result.url.clone(),
headers,
content_length: result.filesize,
};
req.create_async().await
},
}
}
} }
impl From<YoutubeDl<'static>> for Input { impl From<YoutubeDl<'static>> for Input {
@@ -204,34 +238,7 @@ impl Compose for YoutubeDl<'_> {
let mut results = self.query(1).await?; let mut results = self.query(1).await?;
let result = results.swap_remove(0); let result = results.swap_remove(0);
let mut headers = HeaderMap::default(); self.get_stream(&result).await
if let Some(map) = result.http_headers {
headers.extend(map.iter().filter_map(|(k, v)| {
Some((
HeaderName::from_bytes(k.as_bytes()).ok()?,
HeaderValue::from_str(v).ok()?,
))
}));
}
#[allow(clippy::single_match_else)]
match result.protocol.as_deref() {
Some("m3u8_native") => {
let mut req =
HlsRequest::new_with_headers(self.client.clone(), result.url, headers);
req.create()
},
_ => {
let mut req = HttpRequest {
client: self.client.clone(),
request: result.url,
headers,
content_length: result.filesize,
};
req.create_async().await
},
}
} }
fn should_create_async(&self) -> bool { fn should_create_async(&self) -> bool {

View File

@@ -10,16 +10,15 @@
//! Songbird is an async, cross-library compatible voice system for Discord, written in Rust. //! Songbird is an async, cross-library compatible voice system for Discord, written in Rust.
//! The library offers: //! The library offers:
//! * A standalone gateway frontend compatible with [serenity] and [twilight] using the //! * A standalone gateway frontend compatible with [serenity] and [twilight] using the
//! `"gateway"` and `"[serenity/twilight]"` plus `"[rustls/native]"` features. You can even run //! `"gateway"` and `"[serenity/twilight]"` plus `"[rustls/native]"` features. You can even run
//! driverless, to help manage your [lavalink] sessions. //! driverless, to help manage your [lavalink] sessions.
//! * A standalone driver for voice calls, via the `"driver"` feature. If you can create //! * A standalone driver for voice calls, via the `"driver"` feature. If you can create
//! a `ConnectionInfo` using any other gateway, or language for your bot, then you //! a `ConnectionInfo` using any other gateway, or language for your bot, then you
//! can run the songbird voice driver. //! can run the songbird voice driver.
//! * Voice receive and RT(C)P packet handling via the `"receive"` feature. //! * Voice receive and RT(C)P packet handling via the `"receive"` feature.
//! * SIMD-accelerated JSON decoding via the `"simd-json"` feature.
//! * And, by default, a fully featured voice system featuring events, queues, //! * And, by default, a fully featured voice system featuring events, queues,
//! seeking on compatible streams, shared multithreaded audio stream caches, //! seeking on compatible streams, shared multithreaded audio stream caches,
//! and direct Opus data passthrough from DCA files. //! and direct Opus data passthrough from DCA files.
//! //!
//! ## Intents //! ## Intents
//! Songbird's gateway functionality requires you to specify the `GUILD_VOICE_STATES` intent. //! Songbird's gateway functionality requires you to specify the `GUILD_VOICE_STATES` intent.
@@ -38,7 +37,7 @@
//! ```toml //! ```toml
//! # Including songbird alone gives you support for Opus via the DCA file format. //! # Including songbird alone gives you support for Opus via the DCA file format.
//! [dependencies.songbird] //! [dependencies.songbird]
//! version = "0.4" //! version = "0.5"
//! features = ["builtin-queue"] //! features = ["builtin-queue"]
//! //!
//! # To get additional codecs, you *must* add Symphonia yourself. //! # To get additional codecs, you *must* add Symphonia yourself.
@@ -78,6 +77,9 @@
clippy::missing_panics_doc, clippy::missing_panics_doc,
clippy::doc_link_with_quotes, clippy::doc_link_with_quotes,
clippy::doc_markdown, clippy::doc_markdown,
// Allowed as they cannot be fixed without breaking
clippy::result_large_err,
clippy::large_enum_variant,
)] )]
mod config; mod config;

View File

@@ -158,32 +158,30 @@ impl Songbird {
} }
fn get_or_insert_inner(&self, guild_id: GuildId) -> Arc<Mutex<Call>> { fn get_or_insert_inner(&self, guild_id: GuildId) -> Arc<Mutex<Call>> {
self.get(guild_id).unwrap_or_else(|| { self.calls
self.calls .entry(guild_id)
.entry(guild_id) .or_insert_with(|| {
.or_insert_with(|| { let info = self
let info = self .client_data
.client_data .get()
.get() .expect("Manager has not been initialised");
.expect("Manager has not been initialised");
let shard = shard_id(guild_id.0.get(), info.shard_count); let shard = shard_id(guild_id.0.get(), info.shard_count);
let shard_handle = self let shard_handle = self
.sharder .sharder
.get_shard(shard) .get_shard(shard)
.expect("Failed to get shard handle: shard_count incorrect?"); .expect("Failed to get shard handle: shard_count incorrect?");
let call = Call::from_config( let call = Call::from_config(
guild_id, guild_id,
shard_handle, shard_handle,
info.user_id, info.user_id,
self.config.read().clone(), self.config.read().clone(),
); );
Arc::new(Mutex::new(call)) Arc::new(Mutex::new(call))
}) })
.clone() .clone()
})
} }
/// Creates an iterator for all [`Call`]s currently managed. /// Creates an iterator for all [`Call`]s currently managed.