diff --git a/Cargo.lock b/Cargo.lock index 43bd990..cf7e7ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,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.4 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-rt 0.2.5 (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.8 (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.29 (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.10" @@ -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.4" +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.8 (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.29 (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.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "actix-router" version = "0.1.5" @@ -1291,6 +1325,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.4 (registry+https://github.com/rust-lang/crates.io-index)", "actix-web 1.0.8 (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.11.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1706,6 +1742,20 @@ dependencies = [ "trust-dns-proto 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "twoway" +version = "0.2.1" +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" @@ -1881,7 +1931,9 @@ dependencies = [ [metadata] "checksum actix-codec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9f2c11af4b06dc935d8e1b1491dad56bfb32febc49096a91e773f8535c176453" "checksum actix-connect 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "9fade9bd4bb46bacde89f1e726c7a3dd230536092712f5d94d77ca57c087fca0" +"checksum actix-form-data 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8828f26a4e30cad32e16daa098771681590813ca4cdf4be94df6f80d1e6eb81" "checksum actix-http 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "cdf758ebbc4abfecbdc1ce7408601b2d7e0cd7e4766ef61183cd8ce16c194d64" +"checksum actix-multipart 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "12dbaf807ae95268b0ff4e5eb1398c80cd78101211a28ae8b922859961e6ac89" "checksum actix-router 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "23224bb527e204261d0291102cb9b52713084def67d94f7874923baefe04ccf7" "checksum actix-rt 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "168620aaf00fcd2a16e621790abaf180ef7377c2f8355b4ca5775d6afc778ed8" "checksum actix-server 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dd626534af8d0a738e5f74901fe603af0445708f91b86a7d763d80df10d562a5" @@ -2058,6 +2110,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.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" +"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 eb47b11..c6176d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ futures = "0.1.29" rand = "0.7.2" actix-web-httpauth = "0.3.2" syntect = "3.3.0" +actix-multipart = "0.1.3" +actix-form-data = "0.4.0" [dev-dependencies] tempfile = "3.1.0" diff --git a/src/lib.rs b/src/lib.rs index 52dee74..5df91ea 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; @@ -5,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; @@ -15,16 +15,22 @@ 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()) + .max_field_size(MAX_PASTE_SIZE); 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,45 +73,50 @@ 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 || { - 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); - } - }; + (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); + } + }; - 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()), - }) + 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(), + }) } #[get("/{paste_id}")] @@ -253,6 +264,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()); @@ -264,13 +276,32 @@ 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);