diff --git a/Cargo.lock b/Cargo.lock index 081c2ec..32b230a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,44 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-multipart" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee489e3c01eae4d1c35b03c4493f71cb40d93f66b14558feb1b1a807671cc4e" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec592f234db8a253cf80531246a4407c8a70530423eea80688a6c5a44a110e7" +dependencies = [ + "darling 0.14.4", + "parse-size", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "actix-router" version = "0.5.1" @@ -278,6 +316,7 @@ dependencies = [ "actix-cors", "actix-files", "actix-identity", + "actix-multipart", "actix-session", "actix-web", "adf_bdd", @@ -918,8 +957,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", ] [[package]] @@ -936,13 +985,38 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", "quote", "syn 1.0.109", ] @@ -1881,6 +1955,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + [[package]] name = "password-hash" version = "0.5.0" @@ -2339,6 +2419,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2367,7 +2456,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", "syn 1.0.109", diff --git a/server/Cargo.toml b/server/Cargo.toml index b3d681d..d450598 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -25,6 +25,7 @@ argon2 = "0.5.0" actix-session = { version="0.7.2", features = ["cookie-session"] } names = "0.14.0" futures-util = "0.3.28" +actix-multipart = "0.6.0" [features] cors_for_local_development = [] diff --git a/server/src/adf.rs b/server/src/adf.rs index 34ce114..3a90b85 100644 --- a/server/src/adf.rs +++ b/server/src/adf.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; use actix_identity::Identity; +use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; use actix_web::rt::spawn; use actix_web::rt::task::spawn_blocking; use actix_web::rt::time::timeout; @@ -27,13 +28,13 @@ use crate::user::{username_exists, User}; type Ac = Vec; type AcDb = Vec; -#[derive(Copy, Clone, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] pub(crate) enum Parsing { Naive, Hybrid, } -#[derive(Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] pub(crate) enum Strategy { Ground, Complete, @@ -56,6 +57,7 @@ impl From for Bson { } #[derive(Clone, Default, Deserialize, Serialize)] +#[serde(tag = "type", content = "content")] pub(crate) enum OptionWithError { Some(T), Error(String), @@ -182,11 +184,36 @@ pub(crate) struct AdfProblem { pub(crate) acs_per_strategy: AcsPerStrategy, } -#[derive(Clone, Deserialize)] -struct AddAdfProblemBody { - name: Option, +#[derive(MultipartForm)] +struct AddAdfProblemBodyMultipart { + name: Text, + code: Option>, // Either Code or File is set + file: Option, // Either Code or File is set + parsing: Text, +} + +#[derive(Clone)] +struct AddAdfProblemBodyPlain { + name: String, code: String, - parse_strategy: Parsing, + parsing: Parsing, +} + +impl TryFrom for AddAdfProblemBodyPlain { + type Error = &'static str; + + fn try_from(source: AddAdfProblemBodyMultipart) -> Result { + Ok(Self { + name: source.name.into_inner(), + code: source + .file + .map(|f| std::io::read_to_string(f.file).expect("TempFile should be readable")) + .or_else(|| source.code.map(|c| c.into_inner())) + .and_then(|code| (!code.is_empty()).then_some(code)) + .ok_or("Either a file or the code has to be provided.")?, + parsing: source.parsing.into_inner(), + }) + } } async fn adf_problem_exists( @@ -233,9 +260,12 @@ async fn add_adf_problem( req: HttpRequest, app_state: web::Data, identity: Option, - req_body: web::Json, + req_body: MultipartForm, ) -> impl Responder { - let adf_problem_input: AddAdfProblemBody = req_body.into_inner(); + let adf_problem_input: AddAdfProblemBodyPlain = match req_body.into_inner().try_into() { + Ok(input) => input, + Err(err) => return HttpResponse::BadRequest().body(err), + }; let adf_coll: mongodb::Collection = app_state .mongodb_client .database(DB_NAME) @@ -291,35 +321,32 @@ async fn add_adf_problem( Some(Ok(username)) => username, }; - let problem_name = match &adf_problem_input.name { - Some(name) => { - if adf_problem_exists(&adf_coll, name, &username).await { - return HttpResponse::Conflict() - .body("ADF Problem with that name already exists. Please pick another one!"); - } - - name.clone() + let problem_name = if !adf_problem_input.name.is_empty() { + if adf_problem_exists(&adf_coll, &adf_problem_input.name, &username).await { + return HttpResponse::Conflict() + .body("ADF Problem with that name already exists. Please pick another one!"); } - None => { - let gen = Generator::with_naming(Name::Numbered); - let candidates = gen.take(10); - let mut name: Option = None; - for candidate in candidates { - if name.is_some() { - continue; - } + adf_problem_input.name.clone() + } else { + let gen = Generator::with_naming(Name::Numbered); + let candidates = gen.take(10); - if !(adf_problem_exists(&adf_coll, &candidate, &username).await) { - name = Some(candidate); - } + let mut name: Option = None; + for candidate in candidates { + if name.is_some() { + continue; } - match name { - Some(name) => name, - None => { - return HttpResponse::InternalServerError().body("Could not generate new name.") - } + if !(adf_problem_exists(&adf_coll, &candidate, &username).await) { + name = Some(candidate); + } + } + + match name { + Some(name) => name, + None => { + return HttpResponse::InternalServerError().body("Could not generate new name.") } } }; @@ -328,7 +355,7 @@ async fn add_adf_problem( name: problem_name.clone(), username: username.clone(), code: adf_problem_input.code.clone(), - parsing_used: adf_problem_input.parse_strategy, + parsing_used: adf_problem_input.parsing, adf: SimplifiedAdfOpt::None, acs_per_strategy: AcsPerStrategy::default(), }; @@ -362,21 +389,25 @@ async fn add_adf_problem( std::thread::sleep(Duration::from_secs(20)); let parser = AdfParser::default(); - parser.parse()(&adf_problem_input.code) - .map_err(|_| "ADF could not be parsed, double check your input!")?; + let parse_result = parser.parse()(&adf_problem_input.code) + .map_err(|_| "ADF could not be parsed, double check your input!"); - let lib_adf = match adf_problem_input.parse_strategy { - Parsing::Naive => Adf::from_parser(&parser), - Parsing::Hybrid => { - let bd_adf = BdAdf::from_parser(&parser); - bd_adf.hybrid_step_opt(false) - } - }; + let result = parse_result.map(|_| { + let lib_adf = match adf_problem_input.parsing { + Parsing::Naive => Adf::from_parser(&parser), + Parsing::Hybrid => { + let bd_adf = BdAdf::from_parser(&parser); + bd_adf.hybrid_step_opt(false) + } + }; - let ac_and_graph = AcAndGraph { - ac: lib_adf.ac.iter().map(|t| t.0.to_string()).collect(), - graph: lib_adf.into_double_labeled_graph(None), - }; + let ac_and_graph = AcAndGraph { + ac: lib_adf.ac.iter().map(|t| t.0.to_string()).collect(), + graph: lib_adf.into_double_labeled_graph(None), + }; + + (SimplifiedAdf::from_lib_adf(lib_adf), ac_and_graph) + }); app_state .currently_running @@ -384,7 +415,7 @@ async fn add_adf_problem( .unwrap() .remove(&running_info); - Ok::<_, &str>((SimplifiedAdf::from_lib_adf(lib_adf), ac_and_graph)) + result }), ); diff --git a/server/src/config.rs b/server/src/config.rs index e1b4457..b18061b 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -17,13 +17,14 @@ pub(crate) const DB_NAME: &str = "adf-obdd"; pub(crate) const USER_COLL: &str = "users"; pub(crate) const ADF_COLL: &str = "adf-problems"; -#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] +#[serde(tag = "type", content = "content")] pub(crate) enum Task { Parse, Solve(Strategy), } -#[derive(Clone, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub(crate) struct RunningInfo { pub(crate) username: String, pub(crate) adf_name: String, diff --git a/server/src/main.rs b/server/src/main.rs index 34e19ed..fbb7a50 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -43,6 +43,12 @@ async fn main() -> std::io::Result<()> { // cookie secret ket let secret_key = Key::generate(); + // needs to be set outside of httpserver closure to only create it once! + let app_data = web::Data::new(AppState { + mongodb_client: client.clone(), + currently_running: Mutex::new(HashSet::new()), + }); + HttpServer::new(move || { let app = App::new(); @@ -62,37 +68,34 @@ async fn main() -> std::io::Result<()> { #[cfg(not(feature = "cors_for_local_development"))] let cookie_secure = true; - app.app_data(web::Data::new(AppState { - mongodb_client: client.clone(), - currently_running: Mutex::new(HashSet::new()), - })) - .wrap(IdentityMiddleware::default()) - .wrap( - SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) - .cookie_name("adf-obdd-service-auth".to_owned()) - .cookie_secure(cookie_secure) - .session_lifecycle(PersistentSession::default().session_ttl(COOKIE_DURATION)) - .build(), - ) - .service( - web::scope("/users") - .service(register) - .service(delete_account) - .service(login) - .service(logout) - .service(user_info) - .service(update_user), - ) - .service( - web::scope("/adf") - .service(add_adf_problem) - .service(solve_adf_problem) - .service(get_adf_problem) - .service(delete_adf_problem) - .service(get_adf_problems_for_user), - ) - // this mus be last to not override anything - .service(fs::Files::new("/", ASSET_DIRECTORY).index_file("index.html")) + app.app_data(app_data.clone()) + .wrap(IdentityMiddleware::default()) + .wrap( + SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone()) + .cookie_name("adf-obdd-service-auth".to_owned()) + .cookie_secure(cookie_secure) + .session_lifecycle(PersistentSession::default().session_ttl(COOKIE_DURATION)) + .build(), + ) + .service( + web::scope("/users") + .service(register) + .service(delete_account) + .service(login) + .service(logout) + .service(user_info) + .service(update_user), + ) + .service( + web::scope("/adf") + .service(add_adf_problem) + .service(solve_adf_problem) + .service(get_adf_problem) + .service(delete_adf_problem) + .service(get_adf_problems_for_user), + ) + // this mus be last to not override anything + .service(fs::Files::new("/", ASSET_DIRECTORY).index_file("index.html")) }) .bind(("0.0.0.0", 8080))? .run() diff --git a/server/src/user.rs b/server/src/user.rs index 56d0076..82d1609 100644 --- a/server/src/user.rs +++ b/server/src/user.rs @@ -110,39 +110,44 @@ async fn delete_account( .database(DB_NAME) .collection(ADF_COLL); - match identity.map(|id| id.id()) { - None => HttpResponse::Unauthorized().body("You need to login to delete your account."), - Some(Err(err)) => HttpResponse::InternalServerError().body(err.to_string()), - Some(Ok(username)) => { - // Delete all adfs created by user - match adf_coll - .delete_many(doc! { "username": &username }, None) - .await - { - Err(err) => HttpResponse::InternalServerError().body(err.to_string()), - Ok(DeleteResult { - deleted_count: _, .. - }) => { - // Delete actual user - match user_coll - .delete_one(doc! { "username": &username }, None) - .await - { - Ok(DeleteResult { - deleted_count: 0, .. - }) => HttpResponse::InternalServerError() - .body("Account could not be deleted."), - Ok(DeleteResult { - deleted_count: 1, .. - }) => HttpResponse::Ok().body("Account deleted."), - Ok(_) => unreachable!( + match identity { + None => HttpResponse::Unauthorized().body("You are not logged in."), + Some(id) => match id.id() { + Err(err) => HttpResponse::InternalServerError().body(err.to_string()), + Ok(username) => { + // Delete all adfs created by user + match adf_coll + .delete_many(doc! { "username": &username }, None) + .await + { + Err(err) => HttpResponse::InternalServerError().body(err.to_string()), + Ok(DeleteResult { + deleted_count: _, .. + }) => { + // Delete actual user + match user_coll + .delete_one(doc! { "username": &username }, None) + .await + { + Ok(DeleteResult { + deleted_count: 0, .. + }) => HttpResponse::InternalServerError() + .body("Account could not be deleted."), + Ok(DeleteResult { + deleted_count: 1, .. + }) => { + id.logout(); + HttpResponse::Ok().body("Account deleted.") + } + Ok(_) => unreachable!( "delete_one removes at most one entry so all cases are covered already" ), - Err(err) => HttpResponse::InternalServerError().body(err.to_string()), + Err(err) => HttpResponse::InternalServerError().body(err.to_string()), + } } } } - } + }, } } @@ -237,28 +242,33 @@ async fn user_info(app_state: web::Data, identity: Option) - .database(DB_NAME) .collection(USER_COLL); - match identity.map(|id| id.id()) { + match identity { None => { HttpResponse::Unauthorized().body("You need to login get your account information.") } - Some(Err(err)) => HttpResponse::InternalServerError().body(err.to_string()), - Some(Ok(username)) => { - match user_coll - .find_one(doc! { "username": &username }, None) - .await - { - Ok(Some(user)) => { - let info = UserInfo { - username: user.username, - temp: user.password.is_none(), - }; + Some(id) => match id.id() { + Err(err) => HttpResponse::InternalServerError().body(err.to_string()), + Ok(username) => { + match user_coll + .find_one(doc! { "username": &username }, None) + .await + { + Ok(Some(user)) => { + let info = UserInfo { + username: user.username, + temp: user.password.is_none(), + }; - HttpResponse::Ok().json(info) + HttpResponse::Ok().json(info) + } + Ok(None) => { + id.logout(); + HttpResponse::NotFound().body("Logged in user does not exist anymore.") + } + Err(err) => HttpResponse::InternalServerError().body(err.to_string()), } - Ok(None) => HttpResponse::NotFound().body("Logged in user does not exist anymore."), - Err(err) => HttpResponse::InternalServerError().body(err.to_string()), } - } + }, } }