From 913f468ab4c934c9449ac206b059e62cbbcef162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C4=9Fmur=20Oymak?= Date: Sun, 25 Aug 2019 12:09:20 +0300 Subject: [PATCH 1/7] Add new dependencies actix-multipart and actix-form-data --- Cargo.lock | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 56 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 22c2808..4729d83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,23 @@ dependencies = [ "trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "actix-form-data" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "actix-multipart 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-rt 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-threadpool 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-web 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "http 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "actix-http" version = "0.2.8" @@ -78,6 +95,23 @@ dependencies = [ "trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "actix-multipart" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "actix-service 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-web 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "mime 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "twoway 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "actix-router" version = "0.1.5" @@ -1311,6 +1345,8 @@ dependencies = [ name = "rustpaste" version = "0.1.0" dependencies = [ + "actix-form-data 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-multipart 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web-httpauth 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1726,6 +1762,20 @@ dependencies = [ "trust-dns-proto 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "twoway" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "unchecked-index 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicase" version = "2.4.0" @@ -1892,7 +1942,9 @@ dependencies = [ [metadata] "checksum actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9f2c11af4b06dc935d8e1b1491dad56bfb32febc49096a91e773f8535c176453" "checksum actix-connect 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "646be02316c497d1cb8b1ed82eb723b052d6b3e2462c94bd52d0ac2d4a29a165" +"checksum actix-form-data 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8828f26a4e30cad32e16daa098771681590813ca4cdf4be94df6f80d1e6eb81" "checksum actix-http 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "4d45d3f033fb7ff73467dad0ef1d12afb532b39953d50f4124f4d89451670583" +"checksum actix-multipart 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d2214fd8bab1d40113687caf9075efb122b11a1db82190f569860a2fb44a1e7" "checksum actix-router 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "23224bb527e204261d0291102cb9b52713084def67d94f7874923baefe04ccf7" "checksum actix-rt 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "18d9054b1cfacfa441846b9b99001010cb8fbb02130e6cfdb25cea36d3e98e87" "checksum actix-server 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb3038e9e457e0a498ea682723e0f4e6cc2c4f362a1868d749808355275ad959" @@ -2072,6 +2124,8 @@ dependencies = [ "checksum tokio-udp 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "66268575b80f4a4a710ef83d087fdfeeabdce9b74c797535fbac18a2cb906e92" "checksum trust-dns-proto 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5559ebdf6c2368ddd11e20b11d6bbaf9e46deb803acd7815e93f5a7b4a6d2901" "checksum trust-dns-resolver 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c9992e58dba365798803c0b91018ff6c8d3fc77e06977c4539af2a6bfe0a039" +"checksum twoway 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "766345ed3891b291d01af307cd3ad2992a4261cb6c0c7e665cd3e01cf379dd24" +"checksum unchecked-index 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" "checksum unicase 2.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a84e5511b2a947f3ae965dcb29b13b7b1691b6e7332cf5dbc1744138d5acb7f6" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" diff --git a/Cargo.toml b/Cargo.toml index 57a7214..12f5553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ futures = "0.1.28" rand = "0.7.0" actix-web-httpauth = "0.3.2" syntect = "3.2.1" +actix-multipart = "0.1.3" +actix-form-data = "0.4.0" [dev-dependencies] tempfile = "3.1.0" From 4ff69c11d17ee7610735de5fa9b69a81d6b9b36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C4=9Fmur=20Oymak?= Date: Sun, 25 Aug 2019 14:26:21 +0300 Subject: [PATCH 2/7] Use multipart form data in POST requests --- src/lib.rs | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 52dee74..d712294 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +use actix_multipart::Multipart; use actix_web::dev::ServiceRequest; use actix_web::{get, web, App, HttpResponse, HttpServer}; use actix_web_httpauth::extractors::basic::BasicAuth; @@ -21,10 +22,12 @@ pub fn run(config: Config) -> Result<(), Box> { let auth_config = web::Data::new( actix_web_httpauth::extractors::basic::Config::default().realm("rustpaste pastebin"), ); + let form = form_data::Form::new().field("paste", form_data::Field::text()); HttpServer::new(move || { let basic_auth = HttpAuthentication::basic(authenticate); App::new() + .data(form.clone()) .register_data(config.clone()) .register_data(auth_config.clone()) .service( @@ -67,19 +70,19 @@ impl Config { } } -#[derive(Deserialize)] -struct Paste { - pub data: String, -} - -// TODO: Consider using multipart formdata instead of urlencoded. // XXX: This will hang in an infinite loop if the paste directory does not exist. // We'll probably make sure it exists while parsing config, not here. fn new_paste( config: web::Data, - paste: web::Form, -) -> impl Future { - web::block(move || { + (mp, form): (Multipart, web::Data), +) -> impl Future { + form_data::handle_multipart(mp, form.get_ref().clone()).map(move |form_value| { + // XXX: Can we safely unwrap form_value, thus avoiding this check? + let paste = match form_value.text() { + Some(paste) => paste, + None => return HttpResponse::InternalServerError().into(), + }; + let mut rng = thread_rng(); // Paste IDs (= paste file names) are 8 character alphanumeric strings. @@ -97,14 +100,14 @@ fn new_paste( }; let paste_url = format!("{}/{}", config.url_base, paste_id); - file.write_all(paste.data.as_bytes()).and(Ok(paste_url)) - }) - .then(|res| match res { - Ok(paste_url) => Ok(HttpResponse::Created() - .set_header("Location", paste_url.clone()) - .content_type("text/plain") - .body(paste_url)), - Err(_) => Ok(HttpResponse::InternalServerError().into()), + + match file.write_all(paste.as_bytes()) { + Ok(_) => HttpResponse::Created() + .set_header("Location", paste_url.clone()) + .content_type("text/plain") + .body(paste_url), + Err(_) => HttpResponse::InternalServerError().into(), + } }) } From 99fdbb207a848595854a410c4f4ebd61b113cdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C4=9Fmur=20Oymak?= Date: Sun, 25 Aug 2019 16:24:47 +0300 Subject: [PATCH 3/7] Get multipart handling to work --- src/lib.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d712294..d5a26ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,6 @@ use actix_web_httpauth::middleware::HttpAuthentication; use futures::{future, Future}; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use serde::Deserialize; use std::error::Error; use std::fs; use std::fs::OpenOptions; @@ -77,10 +76,9 @@ fn new_paste( (mp, form): (Multipart, web::Data), ) -> impl Future { form_data::handle_multipart(mp, form.get_ref().clone()).map(move |form_value| { - // XXX: Can we safely unwrap form_value, thus avoiding this check? - let paste = match form_value.text() { - Some(paste) => paste, - None => return HttpResponse::InternalServerError().into(), + let paste = match form_value { + form_data::Value::Map(mut form_map) => form_map.remove("paste")?.text()?, + _ => return None, }; let mut rng = thread_rng(); @@ -99,15 +97,18 @@ fn new_paste( } }; - let paste_url = format!("{}/{}", config.url_base, paste_id); + file.write_all(paste.as_bytes()).ok()?; - match file.write_all(paste.as_bytes()) { - Ok(_) => HttpResponse::Created() - .set_header("Location", paste_url.clone()) - .content_type("text/plain") - .body(paste_url), - Err(_) => HttpResponse::InternalServerError().into(), - } + let paste_url = format!("{}/{}", config.url_base, paste_id); + Some(HttpResponse::Created() + .set_header("Location", paste_url.clone()) + .content_type("text/plain") + .body(paste_url)) + }) + .map(|res| match res { + Some(response) => response, + // TODO: Add info to error response. + None => HttpResponse::InternalServerError().finish(), }) } From 5cd882f299c270094ad701d9f18e44a50917de27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C4=9Fmur=20Oymak?= Date: Tue, 5 Nov 2019 18:11:16 +0300 Subject: [PATCH 4/7] Set max field size for multipart form data --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d5a26ac..27729f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,13 +15,15 @@ use syntect::highlighting::{Color, ThemeSet}; use syntect::html::highlighted_html_for_string; use syntect::parsing::SyntaxSet; +const MAX_PASTE_SIZE: usize = 1_000_000; + pub fn run(config: Config) -> Result<(), Box> { let config = web::Data::new(config); // Realm is hardcoded for now. I will consider getting it from the config file. let auth_config = web::Data::new( actix_web_httpauth::extractors::basic::Config::default().realm("rustpaste pastebin"), ); - let form = form_data::Form::new().field("paste", form_data::Field::text()); + let form = form_data::Form::new().field("paste", form_data::Field::text()).max_field_size(MAX_PASTE_SIZE); HttpServer::new(move || { let basic_auth = HttpAuthentication::basic(authenticate); From fe63fd4f0ec4647e1b84f253c61b44c48d2e677b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C4=9Fmur=20Oymak?= Date: Tue, 5 Nov 2019 19:08:55 +0300 Subject: [PATCH 5/7] Try to test multipart paste (does not work) --- src/lib.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 27729f1..a6b8ee7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -270,13 +270,23 @@ mod tests { .route("/", web::post().to_async(new_paste)), ); - // XXX: This is not urlencoded, but it seems to work. Why? - let paste_content = "hebele hubele\nbubele mubele\n"; + // XXX: Gotta be another way... + // ...but I think I cannot simply set headers by using the API + let paste_boundary = "------------------------020e0f16f7f8376c"; + let paste_headers = + "\nContent-Disposition: form-data; name=\"paste\"; filename=\"tits\"\n\ + Content-Type: application/octet-stream\n\n"; + let paste_content = "🎵 When it's in my face\nI feel all my love and hate 🎵\n\n"; + let paste_payload = [paste_boundary, paste_headers, paste_content, paste_boundary, "--"].join(""); + println!("{}", paste_payload); let req = test::TestRequest::post() - .header("content-type", "application/x-www-form-urlencoded") - .set_payload(format!("data={}", paste_content)) + .header("content-type", format!("multipart/form-data; boundary={}", paste_boundary)) + .header("content-length", "256") + .header("accept", "*/*") + .set_payload(paste_payload) .to_request(); + println!("{:?}", req); let resp = test::call_service(&mut app, req); assert_eq!(resp.status(), http::StatusCode::CREATED); From 85fa9cef7d5d02f7c88d4426c16fa53cf18272fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C4=9Fmur=20Oymak?= Date: Tue, 5 Nov 2019 19:10:03 +0300 Subject: [PATCH 6/7] Ignore test for paste posting I could not write a proper test for multipart paste and it already took too much time. Seems like more trouble than it's worth, so I'll probably try to test this method another way. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index a6b8ee7..0400cfb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -259,6 +259,7 @@ mod tests { } #[test] + #[ignore] fn post_paste_short_text() { let test_dir = TempDir::new().unwrap(); let config = make_test_config(test_dir.path().to_str().unwrap()); From 68fb3c3fb3ec4ae58a1f9950cd21af64eb94f495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ya=C4=9Fmur=20Oymak?= Date: Tue, 5 Nov 2019 21:56:17 +0300 Subject: [PATCH 7/7] Fix style (cargo fmt) --- src/lib.rs | 92 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0400cfb..5df91ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,9 @@ pub fn run(config: Config) -> Result<(), Box> { let auth_config = web::Data::new( actix_web_httpauth::extractors::basic::Config::default().realm("rustpaste pastebin"), ); - let form = form_data::Form::new().field("paste", form_data::Field::text()).max_field_size(MAX_PASTE_SIZE); + let form = form_data::Form::new() + .field("paste", form_data::Field::text()) + .max_field_size(MAX_PASTE_SIZE); HttpServer::new(move || { let basic_auth = HttpAuthentication::basic(authenticate); @@ -77,41 +79,44 @@ fn new_paste( config: web::Data, (mp, form): (Multipart, web::Data), ) -> impl Future { - form_data::handle_multipart(mp, form.get_ref().clone()).map(move |form_value| { - let paste = match form_value { - form_data::Value::Map(mut form_map) => form_map.remove("paste")?.text()?, - _ => return None, - }; - - let mut rng = thread_rng(); - - // Paste IDs (= paste file names) are 8 character alphanumeric strings. - // Here we generate a random ID that is not already in use, - // and create (and open) a paste file with that ID as its name. - let (mut file, paste_id) = loop { - let id: String = iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .take(8) - .collect(); - let full_path = format!("{}/{}", config.paste_dir, id); - if let Ok(file) = OpenOptions::new().write(true).create(true).open(full_path) { - break (file, id); - } - }; + form_data::handle_multipart(mp, form.get_ref().clone()) + .map(move |form_value| { + let paste = match form_value { + form_data::Value::Map(mut form_map) => form_map.remove("paste")?.text()?, + _ => return None, + }; + + let mut rng = thread_rng(); + + // Paste IDs (= paste file names) are 8 character alphanumeric strings. + // Here we generate a random ID that is not already in use, + // and create (and open) a paste file with that ID as its name. + let (mut file, paste_id) = loop { + let id: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(8) + .collect(); + let full_path = format!("{}/{}", config.paste_dir, id); + if let Ok(file) = OpenOptions::new().write(true).create(true).open(full_path) { + break (file, id); + } + }; - file.write_all(paste.as_bytes()).ok()?; + file.write_all(paste.as_bytes()).ok()?; - let paste_url = format!("{}/{}", config.url_base, paste_id); - Some(HttpResponse::Created() - .set_header("Location", paste_url.clone()) - .content_type("text/plain") - .body(paste_url)) - }) - .map(|res| match res { - Some(response) => response, - // TODO: Add info to error response. - None => HttpResponse::InternalServerError().finish(), - }) + let paste_url = format!("{}/{}", config.url_base, paste_id); + Some( + HttpResponse::Created() + .set_header("Location", paste_url.clone()) + .content_type("text/plain") + .body(paste_url), + ) + }) + .map(|res| match res { + Some(response) => response, + // TODO: Add info to error response. + None => HttpResponse::InternalServerError().finish(), + }) } #[get("/{paste_id}")] @@ -274,15 +279,24 @@ mod tests { // XXX: Gotta be another way... // ...but I think I cannot simply set headers by using the API let paste_boundary = "------------------------020e0f16f7f8376c"; - let paste_headers = - "\nContent-Disposition: form-data; name=\"paste\"; filename=\"tits\"\n\ - Content-Type: application/octet-stream\n\n"; + let paste_headers = "\nContent-Disposition: form-data; name=\"paste\"; filename=\"tits\"\n\ + Content-Type: application/octet-stream\n\n"; let paste_content = "🎵 When it's in my face\nI feel all my love and hate 🎵\n\n"; - let paste_payload = [paste_boundary, paste_headers, paste_content, paste_boundary, "--"].join(""); + let paste_payload = [ + paste_boundary, + paste_headers, + paste_content, + paste_boundary, + "--", + ] + .join(""); println!("{}", paste_payload); let req = test::TestRequest::post() - .header("content-type", format!("multipart/form-data; boundary={}", paste_boundary)) + .header( + "content-type", + format!("multipart/form-data; boundary={}", paste_boundary), + ) .header("content-length", "256") .header("accept", "*/*") .set_payload(paste_payload)