diff --git a/Cargo.toml b/Cargo.toml index d3ef762..a8dbb24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "gday_server", "gday_hole_punch", "gday_encryption", - "gday_encryption_async", "gday_contact_exchange_protocol", "gday_file_offer_protocol", ] diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..f6d8997 --- /dev/null +++ b/TODO.md @@ -0,0 +1,9 @@ +- Switch to a simple self-describing binary protocol such as MessagePack or CBOR. +- Figure out a simple way to use these formats with async. +- Have the hole puncher time out and return a helpful error after a given amount of time. +- Have the hole puncher prefer local sockets over public sockets. + +- Allow the server to run without TLS. +- Allow the client to not use TLS. +- Improve error message everywhere in general. Have helpful tips to the user. +- Add checks in the file transfer to avoid TOCTOU bugs. \ No newline at end of file diff --git a/gday/src/base32.rs b/gday/src/base32.rs index 74b1b94..f3104e8 100644 --- a/gday/src/base32.rs +++ b/gday/src/base32.rs @@ -139,7 +139,7 @@ fn encode(mut num: u64) -> String { s.push(b'0'); } - String::from_utf8(s).unwrap() + String::from_utf8(s).expect("Base 32 string is invalid. This shouldn't ever happen.") } #[derive(Error, Debug)] diff --git a/gday/src/transfer.rs b/gday/src/transfer.rs index e2aee53..420d038 100644 --- a/gday/src/transfer.rs +++ b/gday/src/transfer.rs @@ -178,7 +178,7 @@ fn create_progress_bar(bytes: u64) -> ProgressBar { let style = ProgressStyle::with_template( "{msg} [{wide_bar}] {bytes}/{total_bytes} | {bytes_per_sec} | eta: {eta}", ) - .unwrap(); + .expect("Progress bar style string was invalid."); let draw = ProgressDrawTarget::stderr_with_hz(2); ProgressBar::with_draw_target(Some(bytes), draw) .with_style(style) diff --git a/gday_encryption/src/lib.rs b/gday_encryption/src/lib.rs index 8ad6bbc..d9ae701 100644 --- a/gday_encryption/src/lib.rs +++ b/gday_encryption/src/lib.rs @@ -143,7 +143,9 @@ impl EncryptedStream { .encrypt_next_in_place(&[], &mut msg) .map_err(|_| std::io::Error::new(ErrorKind::InvalidData, "Encryption error"))?; - let len = u16::try_from(msg.len()).unwrap().to_be_bytes(); + let len = u16::try_from(msg.len()) + .expect("unreachable: Length of message buffer should always fit in u16") + .to_be_bytes(); // write length to header self.to_send[0..2].copy_from_slice(&len); @@ -160,7 +162,9 @@ impl EncryptedStream { self.is_flushing = false; // make space for new header - self.to_send.extend_from_slice(&[0, 0]).unwrap(); + self.to_send + .extend_from_slice(&[0, 0]) + .expect("unreachable: to_send must have space for the header."); Ok(()) } } @@ -209,7 +213,9 @@ impl Write for EncryptedStream { } let bytes_taken = std::cmp::min(buf.len(), self.to_send.spare_capacity().len() - TAG_SIZE); - self.to_send.extend_from_slice(&buf[..bytes_taken]).unwrap(); + self.to_send.extend_from_slice(&buf[..bytes_taken]).expect( + "unreachable: bytes_taken is less than or equal to to_send.spare_capacity().len()", + ); if self.to_send.spare_capacity().len() == TAG_SIZE { self.flush_write_buf()?; diff --git a/gday_encryption_async/Cargo.toml b/gday_encryption_async/Cargo.toml deleted file mode 100644 index d1445ef..0000000 --- a/gday_encryption_async/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "gday_encryption_async" -version = "0.1.0" -authors = ["Marcin Anforowicz"] -edition = "2021" -description = "A simple async ChaCha20Poly1305 encryption wrapper around an IO stream." -license = "MIT" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -chacha20poly1305 = { version = "0.10.1", features = ["stream"] } -pin-project-lite = "0.2.13" -tokio = "1.36.0" - -[dev-dependencies] -chacha20poly1305 = { version = "0.10.1", default-features = false } -tokio = { version = "1.36.0", features = ["io-util", "test-util", "macros"] } diff --git a/gday_encryption_async/README.md b/gday_encryption_async/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/gday_encryption_async/src/helper_buf.rs b/gday_encryption_async/src/helper_buf.rs deleted file mode 100644 index 57fb689..0000000 --- a/gday_encryption_async/src/helper_buf.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! TODO: ADD DOC -use chacha20poly1305::aead; -use std::ops::{Deref, DerefMut}; - -/// Buffer for storing bytes. -pub struct HelperBuf { - buf: Box<[u8]>, - l_cursor: usize, - r_cursor: usize, -} - -impl HelperBuf { - /// Creates a new [`HelperBuf`] with `capacity`. - pub fn with_capacity(capacity: usize) -> Self { - Self { - buf: vec![0; capacity].into_boxed_slice(), - l_cursor: 0, - r_cursor: 0, - } - } - - /// Removes the first `num_bytes` bytes. - /// Panics if `num_bytes` > `self.len()` - pub fn consume(&mut self, num_bytes: usize) { - self.l_cursor += num_bytes; - assert!(self.l_cursor <= self.r_cursor); - - // if there is now no data stored, - // move cursor to beginning - if self.l_cursor == self.r_cursor { - self.l_cursor = 0; - self.r_cursor = 0; - } - } - - /// Use after putting data to `spare_capacity()`. - pub fn increase_len(&mut self, size: usize) { - self.r_cursor += size; - assert!(self.r_cursor <= self.buf.len()); - } - - /// Returns the internal spare capacity after the stored data. - pub fn spare_capacity(&mut self) -> &mut [u8] { - &mut self.buf[self.r_cursor..] - } - - /// Moves the stored data to the beginning of the internal buffer. - /// Maximizes `spare_capacity_len()` without changing anything else. - pub fn left_align(&mut self) { - self.buf.copy_within(self.l_cursor..self.r_cursor, 0); - self.r_cursor -= self.l_cursor; - self.l_cursor = 0; - } - - /// Returns a mutable view into the part of this - /// buffer starting at index `i`. - pub fn split_off_aead_buf(&mut self, i: usize) -> HelperBufPart { - let start_i = self.l_cursor + i; - HelperBufPart { - parent: self, - start_i, - } - } -} - -impl aead::Buffer for HelperBuf { - fn extend_from_slice(&mut self, other: &[u8]) -> aead::Result<()> { - let new_r_cursor = self.r_cursor + other.len(); - if new_r_cursor > self.buf.len() { - return Err(aead::Error); - } - self.buf[self.r_cursor..new_r_cursor].copy_from_slice(other); - self.r_cursor = new_r_cursor; - Ok(()) - } - - fn truncate(&mut self, len: usize) { - let new_r_cursor = self.l_cursor + len; - assert!(new_r_cursor <= self.r_cursor); - self.r_cursor = new_r_cursor; - } -} - -// The 4 following impls let the user treat this -// struct as a slice with the data-containing portion -impl Deref for HelperBuf { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.buf[self.l_cursor..self.r_cursor] - } -} - -impl DerefMut for HelperBuf { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.buf[self.l_cursor..self.r_cursor] - } -} - -impl AsRef<[u8]> for HelperBuf { - fn as_ref(&self) -> &[u8] { - self - } -} - -impl AsMut<[u8]> for HelperBuf { - fn as_mut(&mut self) -> &mut [u8] { - self - } -} - -/// A mutable view into the back part of a `HelperBuf`. -pub struct HelperBufPart<'a> { - /// The `HelperBuf` this struct references. - parent: &'a mut HelperBuf, - /// The index in `parent` where this view begins. - start_i: usize, -} - -impl<'a> aead::Buffer for HelperBufPart<'a> { - fn extend_from_slice(&mut self, other: &[u8]) -> aead::Result<()> { - let new_r_cursor = self.parent.r_cursor + other.len(); - if new_r_cursor > self.parent.buf.len() { - return Err(aead::Error); - } - self.parent.buf[self.parent.r_cursor..new_r_cursor].copy_from_slice(other); - self.parent.r_cursor = new_r_cursor; - Ok(()) - } - - fn truncate(&mut self, len: usize) { - let new_r_cursor = self.start_i + len; - assert!(new_r_cursor <= self.parent.r_cursor); - self.parent.r_cursor = new_r_cursor; - } -} - -// The 4 following impls let the user treat this -// struct as a slice with the data-containing portion -impl<'a> Deref for HelperBufPart<'a> { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.parent.buf[self.start_i..self.parent.r_cursor] - } -} - -impl<'a> DerefMut for HelperBufPart<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.parent.buf[self.start_i..self.parent.r_cursor] - } -} - -impl<'a> AsRef<[u8]> for HelperBufPart<'a> { - fn as_ref(&self) -> &[u8] { - self - } -} - -impl<'a> AsMut<[u8]> for HelperBufPart<'a> { - fn as_mut(&mut self) -> &mut [u8] { - self - } -} - -#[cfg(test)] -mod tests { - use crate::helper_buf::HelperBuf; - use chacha20poly1305::aead::Buffer; - - #[test] - fn test_helper_buf() { - let mut buf = HelperBuf::with_capacity(4); - assert_eq!(buf.buf.len(), 4); - assert!(buf.is_empty()); - assert_eq!(buf.spare_capacity().len(), 4); - - buf.extend_from_slice(&[1, 2, 3]).unwrap(); - assert_eq!(buf[..], [1, 2, 3][..]); - assert_eq!(*buf.buf, [1, 2, 3, 0]); - assert_eq!(buf.spare_capacity().len(), 1); - - buf.consume(1); - assert_eq!(buf[..], [2, 3][..]); - assert_eq!(*buf.buf, [1, 2, 3, 0]); - assert_eq!(buf.spare_capacity().len(), 1); - - buf.left_align(); - assert_eq!(buf[..], [2, 3][..]); - assert_eq!(*buf.buf, [2, 3, 3, 0]); - assert_eq!(buf.spare_capacity().len(), 2); - - buf.consume(1); - assert_eq!(buf[..], [3][..]); - assert_eq!(*buf.buf, [2, 3, 3, 0]); - assert_eq!(buf.spare_capacity().len(), 2); - } -} diff --git a/gday_encryption_async/src/lib.rs b/gday_encryption_async/src/lib.rs deleted file mode 100644 index 85e1018..0000000 --- a/gday_encryption_async/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![forbid(unsafe_code)] -#![warn(clippy::all)] -//! TODO: Add DOC - -mod helper_buf; -mod reader; -mod writer; - -pub use reader::ReadHalf; -pub use writer::WriteHalf; - -#[cfg(test)] -mod test; diff --git a/gday_encryption_async/src/reader.rs b/gday_encryption_async/src/reader.rs deleted file mode 100644 index 77f5831..0000000 --- a/gday_encryption_async/src/reader.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::helper_buf::HelperBuf; -use chacha20poly1305::aead::stream::DecryptorBE32; -use chacha20poly1305::aead::Buffer; -use chacha20poly1305::ChaCha20Poly1305; -use pin_project_lite::pin_project; -use std::io::ErrorKind; -use std::pin::Pin; -use std::task::{ready, Context, Poll}; -use tokio::io::{AsyncBufRead, AsyncRead, ReadBuf}; - -pin_project! { - /// Encrypted reader - pub struct ReadHalf -{ - #[pin] - inner: T, - decryptor: DecryptorBE32, - plaintext: HelperBuf, - ciphertext: HelperBuf, - } -} - -impl ReadHalf { - pub fn new(inner: T, key: &[u8; 32], nonce: &[u8; 7]) -> Self { - let decryptor = DecryptorBE32::new(key.into(), nonce.into()); - Self { - inner, - decryptor, - plaintext: HelperBuf::with_capacity(u16::MAX as usize + 2), - ciphertext: HelperBuf::with_capacity(u16::MAX as usize + 2), - } - } - - /// Reads at least 1 new chunk into `self.plaintext`. - /// Otherwise returns `Poll::pending` - fn read(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let mut me = self.as_mut().project(); - - // ensure at least a 2-byte header will fit in - // the spare `ciphertext` capacity - if me.ciphertext.spare_capacity().len() <= 2 { - me.ciphertext.left_align(); - } - - // read at least the first 2-byte header - while me.ciphertext.len() < 2 { - let mut read_buf = ReadBuf::new(me.ciphertext.spare_capacity()); - ready!(me.inner.as_mut().poll_read(cx, &mut read_buf))?; - let bytes_read = read_buf.filled().len(); - me.ciphertext.increase_len(bytes_read); - } - - // determine the length of the first chunk - let chunk_len: [u8; 2] = me.ciphertext[0..2].try_into().expect("unreachable"); - let chunk_len = u16::from_be_bytes(chunk_len) as usize + 2; - - // left-align if `chunk_len` won't fit - if me.ciphertext.len() + me.ciphertext.spare_capacity().len() < chunk_len { - me.ciphertext.left_align(); - } - - // read at least one full chunk - while me.ciphertext.len() < chunk_len { - let mut read_buf = ReadBuf::new(me.ciphertext.spare_capacity()); - ready!(me.inner.as_mut().poll_read(cx, &mut read_buf))?; - let bytes_read = read_buf.filled().len(); - me.ciphertext.increase_len(bytes_read); - } - - self.as_mut().decrypt_all_available()?; - Poll::Ready(Ok(())) - } - - /// Decrypts all the full chunks in `self.ciphertext`, and - /// moves them into `self.plaintext` - fn decrypt_all_available(self: Pin<&mut Self>) -> std::io::Result<()> { - let this = self.project(); - // while there's another full encrypted chunk: - while let Some(cipher_chunk) = peek_cipher_chunk(this.ciphertext) { - // exit if there isn't enough room to put the - // decrypted plaintext - if this.plaintext.spare_capacity().len() < cipher_chunk.len() { - return Ok(()); - } - - // decrypt in `self.plaintext` - let mut decryption_space = this.plaintext.split_off_aead_buf(this.plaintext.len()); - - decryption_space - .extend_from_slice(cipher_chunk) - .expect("Unreachable"); - - this.ciphertext.consume(cipher_chunk.len() + 2); - - this.decryptor - .decrypt_next_in_place(&[], &mut decryption_space) - .map_err(|_| std::io::Error::new(ErrorKind::InvalidData, "Decryption error"))?; - } - - Ok(()) - } -} - -/// If there is a full chunk at the beginning of `data`, -/// returns it. -fn peek_cipher_chunk(data: &[u8]) -> Option<&[u8]> { - let len: [u8; 2] = data.get(0..2)?.try_into().expect("unreachable"); - let len = u16::from_be_bytes(len) as usize; - data.get(2..2 + len) -} - -impl AsyncRead for ReadHalf { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - // if we're out of plaintext, read more - if self.plaintext.is_empty() { - ready!(self.as_mut().read(cx))?; - } - - let num_bytes = std::cmp::min(self.plaintext.len(), buf.remaining()); - buf.put_slice(&self.plaintext[0..num_bytes]); - self.project().plaintext.consume(num_bytes); - Poll::Ready(Ok(())) - } -} - -impl AsyncBufRead for ReadHalf { - fn poll_fill_buf( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - // if we're out of plaintext, read more - if self.plaintext.is_empty() { - ready!(self.as_mut().read(cx))?; - } - - Poll::Ready(Ok(self.project().plaintext)) - } - - fn consume(self: Pin<&mut Self>, amt: usize) { - self.project().plaintext.consume(amt); - } -} diff --git a/gday_encryption_async/src/test.rs b/gday_encryption_async/src/test.rs deleted file mode 100644 index 9c544a7..0000000 --- a/gday_encryption_async/src/test.rs +++ /dev/null @@ -1,24 +0,0 @@ -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -// todo: test helper buf - -#[tokio::test] -async fn test_all() { - let nonce = [5; 7]; - let key = [5; 32]; - let (read_stream, write_stream) = tokio::io::duplex(400); - let mut buf = [0u8; 3]; - let mut writer = crate::WriteHalf::new(write_stream, &key, &nonce); - let mut reader = crate::ReadHalf::new(read_stream, &key, &nonce); - - let test_data = [ - b"abc", b"def", b"ghi", b"jkl", b"mno", b"prs", b"tuw", b"yzz", - ]; - - for msg in test_data { - writer.write_all(msg).await.unwrap(); - writer.flush().await.unwrap(); - reader.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, msg[..]); - } -} diff --git a/gday_encryption_async/src/writer.rs b/gday_encryption_async/src/writer.rs deleted file mode 100644 index d91ca92..0000000 --- a/gday_encryption_async/src/writer.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::helper_buf::HelperBuf; -use chacha20poly1305::aead::generic_array::typenum::Unsigned; -use chacha20poly1305::aead::stream::EncryptorBE32; -use chacha20poly1305::aead::AeadCore; -use chacha20poly1305::aead::Buffer; -use chacha20poly1305::ChaCha20Poly1305; -use pin_project_lite::pin_project; -use std::{ - io::ErrorKind, - pin::Pin, - task::{ready, Context, Poll}, -}; -use tokio::io::AsyncWrite; - -pin_project! { - /// Encrypted writer - pub struct WriteHalf { - #[pin] - inner: T, - encryptor: EncryptorBE32, - data: HelperBuf, - is_flushing: bool, - } -} - -impl WriteHalf { - pub fn new(inner: T, key: &[u8; 32], nonce: &[u8; 7]) -> Self { - let encryptor = EncryptorBE32::new(key.into(), nonce.into()); - Self { - inner, - encryptor, - data: HelperBuf::with_capacity(2_usize.pow(16) + 2), - is_flushing: true, - } - } - - fn flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let mut me = self.project(); - - // no need to flush if there's no data. - if me.data.len() == 2 { - *me.is_flushing = false; - return Poll::Ready(Ok(())); - } - - // if not flushing, begin flushing - if !*me.is_flushing { - // encrypt in place - let mut msg = me.data.split_off_aead_buf(2); - - me.encryptor - .encrypt_next_in_place(&[], &mut msg) - .map_err(|_| std::io::Error::new(ErrorKind::InvalidData, "Encryption error"))?; - - let len = u16::try_from(msg.len()).unwrap().to_be_bytes(); - - // write length to header - me.data[0..2].copy_from_slice(&len); - - *me.is_flushing = true; - } - - // write until empty or `Poll::Pending` - while !me.data.is_empty() { - let bytes_written = ready!(me.inner.as_mut().poll_write(cx, me.data))?; - me.data.consume(bytes_written); - } - - *me.is_flushing = false; - - // make space for new header - me.data.extend_from_slice(&[0, 0]).unwrap(); - Poll::Ready(Ok(())) - } -} - -impl AsyncWrite for WriteHalf { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - if self.is_flushing { - ready!(self.as_mut().flush(cx))?; - } - - let me = self.as_mut().project(); - - let ciphertext_overhead = ::TagSize::to_usize(); - - let bytes_taken = std::cmp::min( - buf.len(), - me.data.spare_capacity().len() - ciphertext_overhead, - ); - me.data.extend_from_slice(&buf[..bytes_taken]).unwrap(); - - if me.data.spare_capacity().len() == ciphertext_overhead { - let _ = self.as_mut().flush(cx)?; - } - Poll::Ready(Ok(bytes_taken)) - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - ready!(self.as_mut().flush(cx))?; - self.project().inner.poll_flush(cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - ready!(self.as_mut().poll_flush(cx))?; - self.project().inner.poll_shutdown(cx) - } -} diff --git a/gday_hole_punch/src/contact_sharer.rs b/gday_hole_punch/src/contact_sharer.rs index 06d19c2..d82d203 100644 --- a/gday_hole_punch/src/contact_sharer.rs +++ b/gday_hole_punch/src/contact_sharer.rs @@ -1,7 +1,5 @@ use crate::{server_connector::ServerConnection, Error}; -use gday_contact_exchange_protocol::{ - from_reader, to_writer, ClientMsg, FullContact, ServerMsg, -}; +use gday_contact_exchange_protocol::{from_reader, to_writer, ClientMsg, FullContact, ServerMsg}; /// Used to exchange socket addresses with a peer via a Gday server. pub struct ContactSharer { diff --git a/gday_hole_punch/src/hole_puncher.rs b/gday_hole_punch/src/hole_puncher.rs index 4c7a632..570644a 100644 --- a/gday_hole_punch/src/hole_puncher.rs +++ b/gday_hole_punch/src/hole_puncher.rs @@ -28,7 +28,7 @@ const RETRY_INTERVAL: Duration = Duration::from_millis(100); /// Returns: /// - A [`std::net::TcpStream`] to the other peer. /// - A `[u8; 32]` shared key that was derived using -/// [SPAKE2](https://docs.rs/spake2/latest/spake2/) and the weaker `shared_secret`. +/// [SPAKE2](https://docs.rs/spake2/latest/spake2/) and the weaker `shared_secret`. pub fn try_connect_to_peer( local_contact: Contact, peer_contact: FullContact, diff --git a/gday_server/src/state.rs b/gday_server/src/state.rs index eabd886..aefc988 100644 --- a/gday_server/src/state.rs +++ b/gday_server/src/state.rs @@ -83,7 +83,7 @@ impl State { let mut interval = tokio::time::interval(Duration::from_secs(60)); loop { interval.tick().await; - cloned_self.request_counts.lock().unwrap().clear(); + cloned_self.request_counts.lock().expect("Couldn't acquire state lock.").clear(); } }); @@ -97,7 +97,7 @@ impl State { pub fn create_room(&mut self, room_code: u64, origin: IpAddr) -> Result { self.increment_request_count(origin)?; - let mut rooms = self.rooms.lock().unwrap(); + let mut rooms = self.rooms.lock().expect("Couldn't acquire state lock."); // return error if this room code has been taken if rooms.contains_key(&room_code) { @@ -110,7 +110,7 @@ impl State { let cloned_self = self.clone(); tokio::spawn(async move { tokio::time::sleep(timeout).await; - cloned_self.rooms.lock().unwrap().remove(&room_code); + cloned_self.rooms.lock().expect("Couldn't acquire state lock.").remove(&room_code); }); Ok(room_code) @@ -131,7 +131,7 @@ impl State { ) -> Result<(), Error> { self.increment_request_count(origin)?; - let mut rooms = self.rooms.lock().unwrap(); + let mut rooms = self.rooms.lock().expect("Couldn't acquire state lock."); let room = rooms.get_mut(&room_code).ok_or(Error::NoSuchRoomCode)?; let full_contact = &mut room.get_client_mut(is_creator).contact; @@ -167,7 +167,7 @@ impl State { ) -> Result<(FullContact, oneshot::Receiver), Error> { self.increment_request_count(origin)?; - let mut rooms = self.rooms.lock().unwrap(); + let mut rooms = self.rooms.lock().expect("Couldn't acquire state lock."); let room = rooms.get_mut(&room_code).ok_or(Error::NoSuchRoomCode)?; let (tx, rx) = oneshot::channel(); @@ -202,7 +202,7 @@ impl State { /// Returns a [`Error::TooManyRequests`] if [`State::max_requests_per_minute`] /// is exceeded. fn increment_request_count(&mut self, ip: IpAddr) -> Result<(), Error> { - let mut request_counts = self.request_counts.lock().unwrap(); + let mut request_counts = self.request_counts.lock().expect("Couldn't acquire state lock."); let conns_count = request_counts.entry(ip).or_insert(0); if *conns_count > *self.max_requests_per_minute {