Skip to content

Commit ab1634a

Browse files
mikemiles-devmikemiles-dev
authored andcommitted
fix: Added client/server version checking. Clients with mismatched versions are disconnected with a link to upgrade instructions.
1 parent 51e48c7 commit ab1634a

File tree

11 files changed

+119
-6
lines changed

11 files changed

+119
-6
lines changed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ members = [ "server", "client", "shared"]
55
[workspace.package]
66
description = "Rust Chat Application using Tokio over TCP"
77
edition = "2024"
8-
version = "0.1.8"
8+
version = "0.1.9"
99
authors = ["michael.mileusnich@gmail.com"]
1010
readme = "README.md"
1111

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ A modern, colorful terminal-based chat application written in Rust with async/aw
2525
- 🚀 **Production Ready** - Docker and native systemd deployment options
2626
- 👮 **Admin Commands** - Server-side `/kick`, `/ban`, `/rename` and user management
2727
- 📝 **User Status** - Set a custom status message visible to other users
28+
- 🔢 **Version Compatibility** - Client/server version checking with upgrade notifications
2829

2930
## Architecture
3031

@@ -604,6 +605,20 @@ The application implements comprehensive security measures to protect against co
604605

605606
**Note**: For production deployment, TLS encryption is built-in. Consider adding authentication and E2E encryption for enhanced security.
606607

608+
### Version Compatibility
609+
610+
The client and server perform version checking on connection:
611+
- **Automatic Check**: Client sends its version to the server on connect
612+
- **Mismatch Handling**: If versions don't match, server disconnects client with an error
613+
- **Upgrade Instructions**: Error message includes a link to the GitHub README for upgrade instructions
614+
- **Compile-time Version**: Version is automatically derived from `Cargo.toml`
615+
616+
Example version mismatch error:
617+
```
618+
[ERROR] Version mismatch: client v0.1.8 != server v0.1.9
619+
[ERROR] Please upgrade your binary or Docker image. See: https://github.com/mikemiles-dev/rust_chat#readme
620+
```
621+
607622
### Message Protocol
608623

609624
Messages are sent over TCP with a custom chunked protocol that supports:
@@ -615,6 +630,7 @@ Messages are sent over TCP with a custom chunked protocol that supports:
615630
- User list requests
616631
- User status updates
617632
- File transfers
633+
- Version checking
618634
- Error messages
619635

620636
## Building from Source

RELEASES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 0.1.9
2+
* Added client/server version checking. Clients with mismatched versions are disconnected with a link to upgrade instructions.
3+
14
# 0.1.8
25
* Status now persists across reconnections but is cleared on explicit `/quit`, kick, or ban.
36

client/src/client.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use shared::commands::client as commands;
66
use shared::logger;
77
use shared::message::{ChatMessage, ChatMessageError, MessageTypes};
88
use shared::network::{MAX_FILE_SIZE, TcpMessageHandler};
9+
use shared::version::VERSION;
910
use std::collections::HashSet;
1011
use std::io;
1112
use std::net::AddrParseError;
@@ -177,6 +178,15 @@ impl ChatClient {
177178
}
178179

179180
pub async fn join_server(&mut self) -> Result<(), ChatClientError> {
181+
// First send version check
182+
logger::log_info(&format!("Sending version check (v{})...", VERSION));
183+
let version_message = ChatMessage::try_new(
184+
MessageTypes::VersionCheck,
185+
Some(VERSION.as_bytes().to_vec()),
186+
)?;
187+
self.send_message_chunked(version_message).await?;
188+
189+
// Send join message with username
180190
let chat_message =
181191
ChatMessage::try_new(MessageTypes::Join, Some(self.chat_name.as_bytes().to_vec()))?;
182192
self.send_message_chunked(chat_message).await?;
@@ -366,6 +376,31 @@ impl ChatClient {
366376
MessageTypes::Pong => {
367377
// Ignore pong messages (we don't send pings from client)
368378
}
379+
MessageTypes::VersionMismatch => {
380+
if let Some(content) = self.get_message_content(&message, "version mismatch") {
381+
let parts: Vec<&str> = content.split('|').collect();
382+
if parts.len() >= 3 {
383+
logger::log_error(&format!(
384+
"Version mismatch: client v{} != server v{}",
385+
parts[0], parts[1]
386+
));
387+
logger::log_error(&format!(
388+
"Please upgrade your binary or Docker image. See: {}",
389+
parts[2]
390+
));
391+
} else {
392+
logger::log_error(
393+
"Version mismatch with server. Please upgrade your client.",
394+
);
395+
}
396+
// Mark as kicked so we don't try to reconnect
397+
self.was_kicked = true;
398+
return false;
399+
}
400+
}
401+
MessageTypes::VersionCheck => {
402+
// Server shouldn't send this to client, ignore
403+
}
369404
_ => {
370405
logger::log_warning(&format!("Unknown message type: {:?}", message.msg_type));
371406
}

server/src/user_connection/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub enum UserConnectionError {
1010
JoinError,
1111
InvalidMessage,
1212
ExplicitQuit,
13+
VersionMismatch,
1314
}
1415

1516
impl std::fmt::Display for UserConnectionError {
@@ -20,6 +21,7 @@ impl std::fmt::Display for UserConnectionError {
2021
UserConnectionError::JoinError => write!(f, "Join Error: Username already taken"),
2122
UserConnectionError::InvalidMessage => write!(f, "Invalid Message Error"),
2223
UserConnectionError::ExplicitQuit => write!(f, "User explicitly quit"),
24+
UserConnectionError::VersionMismatch => write!(f, "Client/Server version mismatch"),
2325
}
2426
}
2527
}

server/src/user_connection/handlers.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use rand::Rng;
22
use shared::logger;
33
use shared::message::{ChatMessage, MessageTypes};
44
use shared::network::TcpMessageHandler;
5+
use shared::version::{self, VERSION};
56
use std::collections::{HashMap, HashSet};
67
use std::net::{IpAddr, SocketAddr};
78
use std::sync::Arc;
@@ -67,6 +68,10 @@ impl<'a> MessageHandlers<'a> {
6768
}
6869

6970
match message.msg_type {
71+
MessageTypes::VersionCheck => {
72+
self.process_version_check(message.content_as_string(), &mut tcp_handler)
73+
.await?;
74+
}
7075
MessageTypes::Join => {
7176
self.process_join(message.content_as_string(), &mut tcp_handler, chat_name)
7277
.await?;
@@ -610,6 +615,46 @@ impl<'a> MessageHandlers<'a> {
610615

611616
Ok(())
612617
}
618+
619+
async fn process_version_check<S: AsyncRead + AsyncWrite + Unpin>(
620+
&self,
621+
client_version: Option<String>,
622+
tcp_handler: &mut StreamWrapper<'_, S>,
623+
) -> Result<(), UserConnectionError> {
624+
let client_version = client_version.ok_or(UserConnectionError::InvalidMessage)?;
625+
626+
if !version::versions_compatible(&client_version, VERSION) {
627+
logger::log_warning(&format!(
628+
"Version mismatch from {}: client v{} != server v{}",
629+
self.addr, client_version, VERSION
630+
));
631+
632+
// Send version mismatch error with details
633+
let mismatch_content = format!(
634+
"{}|{}|{}",
635+
client_version,
636+
VERSION,
637+
version::GITHUB_README_URL
638+
);
639+
let mismatch_msg = ChatMessage::try_new(
640+
MessageTypes::VersionMismatch,
641+
Some(mismatch_content.into_bytes()),
642+
)
643+
.map_err(|_| UserConnectionError::InvalidMessage)?;
644+
tcp_handler
645+
.send_message_chunked(mismatch_msg)
646+
.await
647+
.map_err(UserConnectionError::IoError)?;
648+
649+
return Err(UserConnectionError::VersionMismatch);
650+
}
651+
652+
logger::log_info(&format!(
653+
"Version check passed for {}: v{}",
654+
self.addr, client_version
655+
));
656+
Ok(())
657+
}
613658
}
614659

615660
#[cfg(test)]

server/src/user_connection/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ impl UserConnection {
173173
self.clear_status_on_disconnect = true;
174174
break;
175175
}
176+
Err(UserConnectionError::VersionMismatch) => {
177+
// Version mismatch - disconnect client (error already sent)
178+
logger::log_warning(&format!("Client {} disconnected due to version mismatch", self.addr));
179+
break;
180+
}
176181
Err(e) => {
177182
logger::log_error(&format!("Error handling message from {}: {:?}", self.addr, e));
178183
}

shared/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "shared"
3-
version = "0.1.0"
4-
edition = "2024"
3+
version.workspace = true
4+
edition.workspace = true
55

66
[dependencies]
77
tokio.workspace = true

shared/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod input;
33
pub mod logger;
44
pub mod message;
55
pub mod network;
6+
pub mod version;

0 commit comments

Comments
 (0)