feat: admin auth, watch games, kick players
This commit is contained in:
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ADMIN_AUTH=""
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@ target
|
|||||||
# Added by cargo
|
# Added by cargo
|
||||||
|
|
||||||
/target
|
/target
|
||||||
|
|
||||||
|
.env
|
||||||
@@ -11,4 +11,6 @@ COPY ./Cargo.toml .
|
|||||||
|
|
||||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||||
|
|
||||||
|
ENV ADMIN_AUTH=""
|
||||||
|
|
||||||
ENTRYPOINT ["./target/x86_64-unknown-linux-musl/release/connect4-moderator-server", "demo"]
|
ENTRYPOINT ["./target/x86_64-unknown-linux-musl/release/connect4-moderator-server", "demo"]
|
||||||
|
|||||||
1
cycle.sh
Executable file
1
cycle.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
docker stop connect4-moderator-server && docker rm connect4-moderator-server && docker image rm joshuafhiggins/connect4-moderator-server:latest && ./docker_build.sh && ./docker_run.sh
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
--name=connect4-moderator-server \
|
--name=connect4-moderator-server \
|
||||||
--restart unless-stopped \
|
--restart unless-stopped \
|
||||||
|
-e ADMIN_AUTH="${ADMIN_AUTH}" \
|
||||||
-p 5102:8080 \
|
-p 5102:8080 \
|
||||||
joshuafhiggins/connect4-moderator-server
|
joshuafhiggins/connect4-moderator-server
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ async def gameloop(socket):
|
|||||||
col = calculate_move(message[1], board, our_color, opponent_color) # Give your function your opponent's move
|
col = calculate_move(message[1], board, our_color, opponent_color) # Give your function your opponent's move
|
||||||
await socket.send(f'PLAY:{col}') # Send your move to the sever
|
await socket.send(f'PLAY:{col}') # Send your move to the sever
|
||||||
|
|
||||||
case 'KICK':
|
# case 'KICK':
|
||||||
print("You have been kicked from the game")
|
# print("You have been kicked from the game")
|
||||||
break
|
# break
|
||||||
|
|
||||||
case 'ERROR':
|
case 'ERROR':
|
||||||
print(f"{message[0]}: {':'.join(message[1:])}")
|
print(f"{message[0]}: {':'.join(message[1:])}")
|
||||||
|
|||||||
95
src/main.rs
95
src/main.rs
@@ -6,17 +6,23 @@ use crate::types::{Color, Match};
|
|||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::num::ParseIntError;
|
||||||
|
use std::process::abort;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::sync::mpsc::error::SendError;
|
use tokio::sync::mpsc::error::SendError;
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
||||||
|
use tokio_tungstenite::tungstenite::Utf8Bytes;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use types::Client;
|
use types::Client;
|
||||||
|
|
||||||
type Clients = Arc<RwLock<HashMap<SocketAddr, Arc<RwLock<Client>>>>>;
|
type Clients = Arc<RwLock<HashMap<SocketAddr, Arc<RwLock<Client>>>>>;
|
||||||
|
type Usernames = Arc<RwLock<HashMap<String, SocketAddr>>>;
|
||||||
type Observers = Arc<RwLock<HashMap<SocketAddr, UnboundedSender<Message>>>>;
|
type Observers = Arc<RwLock<HashMap<SocketAddr, UnboundedSender<Message>>>>;
|
||||||
type Matches = Arc<RwLock<HashMap<u32, Arc<RwLock<Match>>>>>;
|
type Matches = Arc<RwLock<HashMap<u32, Arc<RwLock<Match>>>>>;
|
||||||
|
|
||||||
@@ -25,14 +31,17 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
// Initialize logging
|
// Initialize logging
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
let demo_mode = args.get(1).is_some() && args.get(1).unwrap() == "demo";
|
let demo_mode = args.get(1).is_some() && args.get(1).unwrap() == "demo";
|
||||||
|
let admin_password = env::var("ADMIN_PASSWORD").unwrap_or_else(|_| String::from("admin"));
|
||||||
|
let admin_password = Arc::new(admin_password);
|
||||||
|
|
||||||
let addr = "0.0.0.0:8080";
|
let addr = "0.0.0.0:8080";
|
||||||
let listener = TcpListener::bind(&addr).await?;
|
let listener = TcpListener::bind(&addr).await?;
|
||||||
info!("WebSocket server listening on: {}", addr);
|
info!("WebSocket server listening on: {}", addr);
|
||||||
|
|
||||||
let clients: Clients = Arc::new(RwLock::new(HashMap::new()));
|
let clients: Clients = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
let usernames: Usernames = Arc::new(RwLock::new(HashMap::new()));
|
||||||
let observers: Observers = Arc::new(RwLock::new(HashMap::new()));
|
let observers: Observers = Arc::new(RwLock::new(HashMap::new()));
|
||||||
let matches: Matches = Arc::new(RwLock::new(HashMap::new()));
|
let matches: Matches = Arc::new(RwLock::new(HashMap::new()));
|
||||||
let admin: Arc<RwLock<Option<SocketAddr>>> = Arc::new(RwLock::new(None));
|
let admin: Arc<RwLock<Option<SocketAddr>>> = Arc::new(RwLock::new(None));
|
||||||
@@ -42,9 +51,11 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
stream,
|
stream,
|
||||||
addr,
|
addr,
|
||||||
clients.clone(),
|
clients.clone(),
|
||||||
|
usernames.clone(),
|
||||||
observers.clone(),
|
observers.clone(),
|
||||||
matches.clone(),
|
matches.clone(),
|
||||||
admin.clone(),
|
admin.clone(),
|
||||||
|
admin_password.clone(),
|
||||||
demo_mode,
|
demo_mode,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -56,9 +67,11 @@ async fn handle_connection(
|
|||||||
stream: TcpStream,
|
stream: TcpStream,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
clients: Clients,
|
clients: Clients,
|
||||||
|
usernames: Usernames,
|
||||||
observers: Observers,
|
observers: Observers,
|
||||||
matches: Matches,
|
matches: Matches,
|
||||||
admin: Arc<RwLock<Option<SocketAddr>>>,
|
admin: Arc<RwLock<Option<SocketAddr>>>,
|
||||||
|
admin_password: Arc<String>,
|
||||||
demo_mode: bool,
|
demo_mode: bool,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
info!("New WebSocket connection from: {}", addr);
|
info!("New WebSocket connection from: {}", addr);
|
||||||
@@ -73,7 +86,7 @@ async fn handle_connection(
|
|||||||
// Spawn task to handle outgoing messages
|
// Spawn task to handle outgoing messages
|
||||||
let send_task = tokio::spawn(async move {
|
let send_task = tokio::spawn(async move {
|
||||||
while let Some(msg) = rx.recv().await {
|
while let Some(msg) = rx.recv().await {
|
||||||
if ws_sender.send(msg).await.is_err() {
|
if ws_sender.send(msg.clone()).await.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,6 +121,7 @@ async fn handle_connection(
|
|||||||
|
|
||||||
// not taken
|
// not taken
|
||||||
observers.write().await.remove(&addr);
|
observers.write().await.remove(&addr);
|
||||||
|
usernames.write().await.insert(requested_username.clone(), addr);
|
||||||
clients.write().await.insert(
|
clients.write().await.insert(
|
||||||
addr.to_string().parse()?,
|
addr.to_string().parse()?,
|
||||||
Arc::new(RwLock::new(Client::new(
|
Arc::new(RwLock::new(Client::new(
|
||||||
@@ -350,17 +364,72 @@ async fn handle_connection(
|
|||||||
let _ = send(&tx, &format!("OPPONENT:{}", random_move));
|
let _ = send(&tx, &format!("OPPONENT:{}", random_move));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if text == "GAME:LIST" {
|
else if text == "GAME:LIST" {
|
||||||
todo!()
|
let matches_guard = matches.read().await;
|
||||||
|
let clients_guard = clients.read().await;
|
||||||
|
let mut to_send = "GAME:LIST:".to_string();
|
||||||
|
for match_guard in matches_guard.values() {
|
||||||
|
let a_match = match_guard.read().await;
|
||||||
|
let player1 = clients_guard.get(&a_match.player1).unwrap().read().await;
|
||||||
|
let player2 = clients_guard.get(&a_match.player2).unwrap().read().await;
|
||||||
|
to_send += a_match.id.to_string().as_str();
|
||||||
|
to_send += ","; to_send += player1.username.as_str(); to_send += ",";
|
||||||
|
to_send += player2.username.as_str(); to_send += "|";
|
||||||
|
}
|
||||||
|
|
||||||
|
to_send.remove(to_send.len() - 1);
|
||||||
|
|
||||||
|
let _ = send(&tx, to_send.as_str());
|
||||||
}
|
}
|
||||||
else if text.starts_with("GAME:WATCH:") {
|
else if text.starts_with("GAME:WATCH:") {
|
||||||
todo!()
|
let match_id_parse = text.split(":").collect::<Vec<&str>>()[2].parse::<u32>();
|
||||||
|
match match_id_parse {
|
||||||
|
Ok(match_id) => {
|
||||||
|
let result = watch(&matches, match_id, addr).await;
|
||||||
|
if result.is_err() { let _ = send(&tx, "ERROR:INVALID:WATCH"); }
|
||||||
|
}
|
||||||
|
Err(_) => { let _ = send(&tx, "ERROR:INVALID:WATCH"); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if text.starts_with("ADMIN:AUTH:") {
|
else if text.starts_with("ADMIN:AUTH:") {
|
||||||
todo!()
|
if admin.read().await.is_some() {
|
||||||
|
let _ = send(&tx, "ERROR:INVALID:AUTH");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let password_parse = text.split(":").collect::<Vec<&str>>()[2];
|
||||||
|
if password_parse != *admin_password {
|
||||||
|
let _ = send(&tx, "ERROR:INVALID:AUTH");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut admin_guard = admin.write().await;
|
||||||
|
*admin_guard = Some(addr.to_string().parse()?);
|
||||||
}
|
}
|
||||||
else if text.starts_with("ADMIN:KICK:") {
|
else if text.starts_with("ADMIN:KICK:") {
|
||||||
todo!()
|
if admin.read().await.is_none() || admin.read().await.unwrap() != addr {
|
||||||
|
let _ = send(&tx, "ERROR:INVALID:AUTH");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let kick_username = text.split(":").collect::<Vec<&str>>()[2];
|
||||||
|
|
||||||
|
let usernames_guard = usernames.read().await;
|
||||||
|
let clients_guard = clients.read().await;
|
||||||
|
|
||||||
|
let kick_addr_result = usernames_guard.get(kick_username);
|
||||||
|
match kick_addr_result {
|
||||||
|
Some(kick_addr) => {
|
||||||
|
let kick_client = clients_guard.get(kick_addr).unwrap().read().await;
|
||||||
|
kick_client.connection.send(Message::Close(None))?;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let _ = send(&tx, "ERROR:INVALID:KICK");
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if text == "GAME:TERMINATE" {
|
else if text == "GAME:TERMINATE" {
|
||||||
todo!()
|
todo!()
|
||||||
@@ -437,8 +506,10 @@ async fn broadcast_message(addrs: &Vec<SocketAddr>, observers: &Observers, msg:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) {
|
async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) -> Result<(), String> {
|
||||||
for a_match in &mut matches.write().await.values_mut() {
|
let mut matches_guard = matches.write().await;
|
||||||
|
|
||||||
|
for a_match in &mut matches_guard.values_mut() {
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
for i in 0..a_match.write().await.viewers.len() {
|
for i in 0..a_match.write().await.viewers.len() {
|
||||||
if a_match.write().await.viewers[i] == addr {
|
if a_match.write().await.viewers[i] == addr {
|
||||||
@@ -453,7 +524,13 @@ async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
matches.write().await.get_mut(&new_match_id).unwrap().write().await.viewers.push(addr);
|
let result = matches_guard.get(&new_match_id);
|
||||||
|
if result.is_none() {
|
||||||
|
return Err("Match not found".to_string());
|
||||||
|
}
|
||||||
|
result.unwrap().write().await.viewers.push(addr);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send(tx: &UnboundedSender<Message>, text: &str) -> Result<(), SendError<Message>> {
|
fn send(tx: &UnboundedSender<Message>, text: &str) -> Result<(), SendError<Message>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user