diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 160 | ||||
-rw-r--r-- | src/types.rs | 27 |
2 files changed, 147 insertions, 40 deletions
diff --git a/src/main.rs b/src/main.rs index e972025..6c2f375 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use std::{ error::Error, net::SocketAddr, sync::Arc, + collections::HashMap, }; mod types; @@ -17,8 +18,8 @@ fn main() -> Result<(), Box<dyn Error>> { let conf = Config { cert: "./cert.pem".to_owned(), pkey: "./cert.rsa".to_owned(), + index_pg: "./assets/index.html".to_owned(), room_pg: "./assets/room.html".to_owned(), - form_pg: "./assets/form.html".to_owned(), client_code: "./assets/client.js".to_owned(), stylesheet: "./assets/style.css".to_owned(), socket_addr: ([0,0,0,0],31235).into(), @@ -29,50 +30,117 @@ fn main() -> Result<(), Box<dyn Error>> { #[tokio::main] async fn tokio_main(conf: Config) -> Result<(), Box<dyn Error>> { - // Start the temporary single room - let room = Arc::new(RwLock::new({ - let name = "Testing room".to_string(); - let players = PlayerMap::default(); - let bconf = BoardConf { w: 75, h: 35, mine_ratio: (1, 8) }; - let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel(); - let handle = tokio::spawn(gameloop(cmd_rx, players.clone(), bconf)); - Room { - name, - players, - peer_limit: 32, - board_conf: bconf, - cmd_stream: cmd_tx, - driver: handle, - } - })); - + let rooms: RoomMap = Arc::new(RwLock::new(HashMap::new())); + let public_rooms = Arc::new(RwLock::new(Vec::new())); use warp::*; - let style = path("s.css").and(fs::file(conf.stylesheet.clone())); - let code = path("c.js").and(fs::file(conf.client_code.clone())); - let font = path("f.ttf").map(|| FONT_FILE); - let listing = path("rlist").map(|| "placeholder'em"); - let room_form = path("r").map(|| "yeah placeholder mate"); - let index = path::end().and(fs::file(conf.room_pg.clone())); - - let websocket_route = { - let room = room.clone(); - use warp::*; - path("ws") + let index = path::end().and(fs::file(conf.index_pg.clone())); + let style = path!("s.css").and(fs::file(conf.stylesheet.clone())); + let code = path!("c.js").and(fs::file(conf.client_code.clone())); + let font = path!("f.ttf").map(|| FONT_FILE); + let listing = path!("rlist").map(|| "placeholder'em"); + let rform_recv = { + let rooms = rooms.clone(); + let pubs = public_rooms.clone(); + post().and(path("r")).and(body::content_length_limit(4096)).and(body::form()) + .and_then(move |rinfo: HashMap<String, String>| { + println!("{:?}", rinfo); + let rooms = rooms.clone(); + let pubs = pubs.clone(); + async move { + if let (Some(w),Some(h),Some(num),Some(denom),access,limit) = ( + rinfo.get("rwidth").and_then(|wt| wt.parse::<usize>().ok()), + rinfo.get("rheight").and_then(|ht| ht.parse::<usize>().ok()), + rinfo.get("rration").and_then(|nt| nt.parse::<usize>().ok()), + rinfo.get("rratiod").and_then(|dt| dt.parse::<usize>().ok()), + rinfo.get("raccess"), + rinfo.get("rlimit").and_then(|l| l.parse::<usize>().ok()), + ) { + let board_conf = BoardConf { w, h, mine_ratio: (num,denom) }; + let name = rinfo.get("rname").map(|r| r.to_owned()).unwrap_or(format!("{w}x{h} room")); + + let mut rooms = rooms.write().await; + let uid = types::RoomId::new_in(&rooms); + let players = PlayerMap::default(); + let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel(); + let handle = tokio::spawn(gameloop(cmd_rx, players.clone(), board_conf)); + rooms.insert(uid.clone(), Arc::new(RwLock::new(Room { + name, + players, + peer_limit: match limit { Some(i) => i, None => usize::MAX }, + public: access.is_some(), + driver: handle, + cmd_stream: cmd_tx, + board_conf, + }))); + if access.is_some() { + pubs.write().await.push(uid.clone()); + } + Ok( + hyper::Response::builder() + .status(hyper::StatusCode::SEE_OTHER) + .header(hyper::header::LOCATION, format!("/room/{uid}")) + .body(hyper::Body::empty()) + .unwrap() + ) + } else { Err(reject::custom(BadFormData)) } + } + }) + }; + let room = { + let rooms_ws = rooms.clone(); + let rooms_lobby = rooms.clone(); + let prefix = get().and(path!("room" / String / ..)); + + // Fixme: better errors + prefix.and(path!("ws")) .and(ws()) .and(addr::remote()) - .map(move |ws: warp::ws::Ws, saddr: Option<SocketAddr>| { - let room = room.clone(); - println!("conn from {saddr:?}"); - ws.on_upgrade(move |socket| { - conn::lobby(socket, saddr.expect("socket without address"), room.clone()) - }) + .and_then(move |id: String, websocket: warp::ws::Ws, saddr: Option<SocketAddr>| { + let rooms = rooms_ws.clone(); + async move { + let id = RoomId {0: id}; + match rooms.read().await.get(&id).map(|x| x.clone()) { + Some(r) => { + println!("conn from {saddr:?} into {id}"); + Ok(websocket.on_upgrade(move |socket| { + conn::lobby(socket, saddr.expect("socket without address"), r.clone()) + })) + }, + None => { + println!("conn from {saddr:?} into inexistent room {id}"); + Err(reject()) + } + } + } }) + .or(prefix.and(path::end()) + .and(fs::file(conf.room_pg.clone())) + .then(move |id: String, f: fs::File| { + let rooms = rooms_lobby.clone(); + async move { + if rooms.read().await.contains_key(&RoomId {0: id}) { + f.into_response() + } else { + reply::with_status("No such room", http::StatusCode::BAD_REQUEST).into_response() + } + } + }) + ) }; - let route = any().and(get().and(index).or(style).or(code).or(font).or(listing)).or(post().and(room_form)); - let routes = websocket_route.or(route); - let server = warp::serve(routes) + + let route = get() + .and(index) + .or(style) + .or(code) + .or(font) + .or(listing) + .or(rform_recv) + .or(room) + .recover(error_handler); + + let server = warp::serve(route) .tls() .cert_path(conf.cert) .key_path(conf.pkey) @@ -121,3 +189,19 @@ async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver<MetaMove>, p } } } + +use warp::{ reject::{ Reject, Rejection }, reply::{ self, Reply }, http::StatusCode }; +#[derive(Debug)] +struct BadFormData; +impl Reject for BadFormData {} + +async fn error_handler(err: Rejection) -> Result<impl Reply, std::convert::Infallible> { + if err.is_not_found() { Ok(reply::with_status("No such file", StatusCode::NOT_FOUND)) } + else if let Some(_e) = err.find::<BadFormData>() { + Ok(reply::with_status("Bad form data", StatusCode::BAD_REQUEST)) + } else { + println!("unhandled rejection: {err:?}"); + Ok(reply::with_status("Server error", StatusCode::INTERNAL_SERVER_ERROR)) + } +} + diff --git a/src/types.rs b/src/types.rs index fb9f7ae..df9e168 100644 --- a/src/types.rs +++ b/src/types.rs @@ -16,8 +16,8 @@ use crate::minesweeper; pub struct Config { pub cert: String, pub pkey: String, + pub index_pg: String, pub room_pg: String, - pub form_pg: String, pub client_code: String, pub stylesheet: String, pub socket_addr: SocketAddr, @@ -41,7 +41,8 @@ impl Display for BoardConf { pub struct Room { pub name: String, pub players: PlayerMap, - pub peer_limit: u32, + pub peer_limit: usize, + pub public: bool, pub driver: tokio::task::JoinHandle<()>, pub cmd_stream: CmdTx, pub board_conf: BoardConf, @@ -75,7 +76,29 @@ impl Display for Player { } } +#[derive(Eq, PartialEq, Hash, Debug, Clone)] +pub struct RoomId(pub String); +impl Display for RoomId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl RoomId { + pub fn new_in<T>(map: &HashMap<RoomId, T>) -> Self { + use rand::{ thread_rng, Rng, distributions::Alphanumeric }; + let id = RoomId { 0: thread_rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect::<String>() }; + if map.contains_key(&id) { RoomId::new_in(map) } + else { id } + } +} + pub type CmdTx = tokio::sync::mpsc::UnboundedSender<MetaMove>; +pub type RoomMap = Arc<RwLock<HashMap<RoomId, Arc<RwLock<Room>>>>>; pub type PlayerMapData = Arc<RwLock<HashMap<SocketAddr, Player>>>; #[derive(Debug)] pub struct PlayerMap { |