first commit

This commit is contained in:
JasterV 2020-11-08 18:29:21 +01:00
commit a104cef2a5
12 changed files with 529 additions and 0 deletions

116
client/.gitignore vendored Normal file
View file

@ -0,0 +1,116 @@
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# End of https://www.toptal.com/developers/gitignore/api/node

28
client/package-lock.json generated Normal file
View file

@ -0,0 +1,28 @@
{
"name": "client",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"bootstrap": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz",
"integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ=="
},
"jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
},
"net": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz",
"integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g="
},
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
}
}
}

18
client/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bootstrap": "^4.5.3",
"jquery": "^3.5.1",
"net": "^1.0.2",
"node-fetch": "^2.6.1"
}
}

38
client/src/index.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat</title>
<script src="main.js"></script>
</head>
<body>
<section class="init-room">
<form>
<div class="form-group">
<label for="exampleInputEmail1">Room Id</label>
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" id="exampleInputPassword1">
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label" for="exampleCheck1">Check me out</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</section>
<section style="display: none;">
<div class="chat">
<div class="chat-header"></div>
<div class="chat-body">
</div>
<div class="chat-input"></div>
</div>
</section>
</body>
</html>

12
client/src/main.js Normal file
View file

@ -0,0 +1,12 @@
require('bootstrap');
require('bootstrap/dist/css/bootstrap.css')
const net = require('net');
// const client = new net.Socket();
// connector.connect(PORT, HOST, function () {
// console.log('CONNECTED TO: ' + HOST + ':' + PORT);
// connector.write()
// });
// connector.on('data', function (addr) {
// console.log('DATA: ' + data);
// });

13
server/.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
# Created by https://www.toptal.com/developers/gitignore/api/rust
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
### Rust ###
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# End of https://www.toptal.com/developers/gitignore/api/rust

24
server/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "server"
version = "0.1.0"
authors = ["JasterV <victorcoder2@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "lib"
path = "src/lib.rs"
[[bin]]
name = "rest-api"
path = "src/bin.rs"
[dependencies]
rocket = "0.4"
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "0.8", features = ["serde", "v4"] }
[dependencies.rocket_contrib]
version = "0.4"
default-features = false
features = ["json"]

89
server/src/bin.rs Normal file
View file

@ -0,0 +1,89 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use]
extern crate rocket;
extern crate uuid;
use lib::rooms::rooms_map::RoomsMap;
use rocket::{
http::{RawStr, Status},
response::status::Custom,
State,
};
use rocket_contrib::json::Json;
use serde::Serialize;
use uuid::Uuid;
use std::{
net::SocketAddr,
sync::{Arc, Mutex},
thread,
time::Duration,
};
#[derive(Serialize)]
struct RoomInfo {
addr: SocketAddr,
id: String,
}
#[get("/rooms/<id>")]
fn get_addr(id: &RawStr, rooms: State<Arc<Mutex<RoomsMap>>>) -> Result<String, Custom<String>> {
let arc_clone = _get_state_arc(&rooms);
let rooms = arc_clone.lock().unwrap();
match rooms.get_addr(id) {
Ok(addr) => Ok(addr.to_string()),
Err(_) => Err(Custom(
Status::ImATeapot,
"Can't get the address".to_string(),
)),
}
}
#[get("/rooms?create")]
fn create_room(state: State<Arc<Mutex<RoomsMap>>>) -> Result<Json<RoomInfo>, Custom<String>> {
let arc_clone = _get_state_arc(&state);
let mut rooms = arc_clone.lock().unwrap();
let id = Uuid::new_v4().to_string();
match rooms.start_room(id.clone()) {
Ok(addr) => {
_close_timeout(id.clone(), &state);
Ok(Json(RoomInfo { addr, id }))
}
Err(message) => Err(Custom(Status::Locked, message)),
}
}
#[delete("/rooms/<id>")]
fn close_room(id: String, state: State<Arc<Mutex<RoomsMap>>>) -> Result<String, Custom<String>> {
let arc_clone = _get_state_arc(&state);
let mut rooms = arc_clone.lock().unwrap();
match rooms.close_room(id.clone()) {
Ok(_) => Ok(id),
Err(message) => Err(Custom(Status::Gone, message)),
}
}
fn _get_state_arc<T: Send + Sync>(state: &State<Arc<Mutex<T>>>) -> Arc<Mutex<T>> {
let arc = state.inner();
let arc_clone = Arc::clone(arc);
arc_clone
}
fn _close_timeout(id: String, state: &State<Arc<Mutex<RoomsMap>>>) {
let arc_clone = _get_state_arc(state);
thread::spawn(move || {
thread::sleep(Duration::from_secs(RoomsMap::ROOMS_TIMEOUT));
let mut rooms = arc_clone.lock().unwrap();
rooms.close_room(id).ok();
});
}
fn main() {
rocket::ignite()
.mount("/", routes![get_addr, close_room, create_room])
.manage(Arc::new(Mutex::new(RoomsMap::new())))
.launch();
}

1
server/src/lib.rs Normal file
View file

@ -0,0 +1 @@
pub mod rooms;

2
server/src/rooms/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod room;
pub mod rooms_map;

132
server/src/rooms/room.rs Normal file
View file

@ -0,0 +1,132 @@
use std::io::{ErrorKind, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::thread;
use std::{
string::FromUtf8Error,
sync::mpsc::{channel, sync_channel, Receiver, SendError, Sender, SyncSender},
};
pub struct Room {
rx: Receiver<(String, TcpStream)>,
tx: Sender<(String, TcpStream)>,
clients: Vec<TcpStream>,
}
impl Room {
pub const MAX_CLIENTS: usize = 15;
pub fn new() -> Self {
let (tx, rx) = channel();
Room {
tx,
rx,
clients: vec![],
}
}
pub fn send_broadcast(&mut self, message: String, addr: SocketAddr) {
self.clients = self
.clients
.iter()
.filter_map(|client| {
let buff = message.clone().into_bytes();
if addr == client.local_addr().unwrap() {
let mut client = client.try_clone().unwrap();
client.write_all(&buff).map(|_| client).ok()
} else {
None
}
})
.collect::<Vec<_>>();
}
pub fn start(mut self) -> Result<RoomController, String> {
match TcpListener::bind("127.0.0.1:0") {
Ok(listener) => {
listener
.set_nonblocking(true)
.expect("Error setting non blocking");
let (tx, rx) = sync_channel(1);
let listener = listener.try_clone().unwrap();
let addr = listener.local_addr().unwrap();
thread::spawn(move || {
self._start(listener, rx);
});
Ok(RoomController { addr, closer: tx })
}
Err(_) => Err(String::from("Error binding the socket")),
}
}
fn _shutdown(&mut self) {
self.clients.iter().for_each(|client| {
client.shutdown(std::net::Shutdown::Both).unwrap();
});
self.clients = vec![];
}
fn _start(&mut self, listener: TcpListener, closer: Receiver<()>) {
loop {
//ON ACCEPT
if let Ok((mut socket, addr)) = listener.accept() {
let tx = Sender::clone(&self.tx);
if self.clients.len() < Room::MAX_CLIENTS {
println!("Client {} connected to room!", addr);
self.clients.push(socket.try_clone().unwrap());
thread::spawn(move || Self::_listen_child(socket, tx));
} else {
socket.write_all(b"The room is full").unwrap();
}
}
//ON CLOSE
if let Ok(_) = closer.try_recv() {
self._shutdown();
break;
}
// ON MESSAGE
if let Ok((msg, socket)) = self.rx.try_recv() {
self.send_broadcast(msg, socket.local_addr().unwrap());
}
}
}
fn _listen_child(mut socket: TcpStream, tx: Sender<(String, TcpStream)>) {
loop {
let mut buffer = vec![0; 1024];
match socket.read(&mut buffer) {
Ok(_) => {
if let Ok(message) = Self::parse_request(buffer) {
if message.len() <= 0 {
break;
}
tx.send((message, socket.try_clone().unwrap())).unwrap();
}
}
Err(ref err) if err.kind() == ErrorKind::WouldBlock => (),
Err(_) => {
println!("closing connection with: {}", socket.local_addr().unwrap());
break;
}
}
}
}
fn parse_request(req: Vec<u8>) -> Result<String, FromUtf8Error> {
String::from_utf8(req.into_iter().take_while(|&x| x != 0).collect::<Vec<_>>())
}
}
pub struct RoomController {
addr: SocketAddr,
closer: SyncSender<()>,
}
impl RoomController {
pub fn addr(&self) -> SocketAddr {
self.addr
}
pub fn shutdown_room(&self) -> Result<(), SendError<()>> {
Ok(self.closer.send(())?)
}
}

View file

@ -0,0 +1,56 @@
use std::collections::HashMap;
use std::io::{self, ErrorKind};
use std::net::{SocketAddr};
use super::room::{Room, RoomController};
pub struct RoomsMap(HashMap<String, RoomController>);
impl RoomsMap {
pub const MAX_ROOMS: usize = 10;
pub const ROOMS_TIMEOUT: u64 = 60 * 10; // timeout in seconds
pub fn new() -> Self {
RoomsMap(HashMap::new())
}
pub fn get_addr(&self, id: &str) -> io::Result<SocketAddr> {
match self.0.get(id) {
Some(controller) => Ok(controller.addr()),
None => Err(io::Error::new(ErrorKind::AddrNotAvailable, ":(")),
}
}
pub fn start_room(&mut self, id: String) -> Result<SocketAddr, String> {
if self.0.contains_key(&id) {
Err(String::from("Room already exists"))
} else if self.0.len() >= Self::MAX_ROOMS {
Err(format!("Can't create more than {} rooms", Self::MAX_ROOMS))
} else {
let room = Room::new();
match room.start() {
Ok(controller) => {
let addr = controller.addr();
self.0.insert(id, controller);
Ok(addr)
}
Err(msg) => Err(msg),
}
}
}
pub fn close_room(&mut self, id: String) -> Result<(), String> {
match self.0.get(&id) {
Some(controller) => {
match controller.shutdown_room() {
Ok(_) => {
self.0.remove_entry(&id);
Ok(())
},
Err(_) => Err(String::from("Can't close the room"))
}
}
None => Err(String::from("The room doesnt exists")),
}
}
}