commit 67e03a481f96bbd8509f60e03b1640615889ce77 Author: moparisthebest Date: Tue Aug 7 23:23:15 2018 -0400 Initial commit, partially done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a0ea0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea/ +Cargo.lock +target/ +upload/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c82daa7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "pastebin" +version = "0.0.0" +publish = false + +[dependencies] +rocket = "0.3.14" +rocket_codegen = "0.3.14" +rand = "0.4" +multipart = "0.15.2" +toml = "0.4.6" +adjective_adjective_animal = "0.1.0" +#tokio = "0.1.7" +#tokio-codec = "0.1.0" \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0ca585f --- /dev/null +++ b/readme.md @@ -0,0 +1,23 @@ +Rust Bucket +----------- + +My ideal vision of a pastebin/image/file host, easy to use from the command line with any tools, and from a browser with and without javascript + +Borrows ideas and inspiration from many different pastebins over the years: + + * [ZeroBin](https://github.com/sebsauvage/ZeroBin) (client-side encryption) + * [chefi](https://github.com/colemickens/chefi) (paste with netcat) + * [Rocket.rs example pastebin](https://github.com/SergioBenitez/Rocket/tree/master/examples/pastebin/src) (framework) + * [dank-paste](https://github.com/wpbirney/dank-paste) (multipart/form-data support) + * [ix.io](http://ix.io/) (downloadable client) + +Delete after 1/2 views, 5/10 minutes, 1 hour/day/week/month/year, forever, delete after not viewed within X + +Don't store file type, only used for link, can use any suffix for link + +Store when to delete (how many views + how long), store how many times viewed, store when uploaded (or file creation date?) + +Maybe if we only support 'burn after reading' like zerobin we can use a special created_date to signify this and then never count views, or, another file, since that would interfere with 'delete after X time' + +Crazy random thoughts: + * hash IDs, use un-hashed ID to encrypt paste \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5b21bae --- /dev/null +++ b/src/main.rs @@ -0,0 +1,328 @@ +#![feature(plugin, decl_macro, custom_derive)] +#![plugin(rocket_codegen)] + +extern crate rocket; +extern crate adjective_adjective_animal; +//extern crate tokio; +//extern crate tokio_codec; +extern crate multipart; + +mod paste_id; +mod mpu; +#[cfg(test)] mod tests; + +use std::io; +use std::fs::File; +use std::path::{Path, PathBuf}; + +use rocket::response::NamedFile; + +use rocket::Data; +use rocket::response::content; + +use paste_id::PasteID; +use mpu::MultipartUpload; +use rocket::request::LenientForm; +use std::fs; + +//use tokio_codec::{Decoder, BytesCodec}; +//use tokio::net::TcpListener; +//use tokio::prelude::*; +use std::thread; +use std::net::TcpListener; +use std::io::{Write,Read}; +use std::time::Duration; + +const HOST: &'static str = "http://localhost:8000"; + +const UPLOAD_MAX_SIZE: u64 = 8 * 1024 * 1024; + +fn new_paste() -> (String, String) { + let id = PasteID::new(); + let filename = format!("upload/{id}", id = id); + let url = format!("{host}/{id}\n", host = HOST, id = id); + (filename, url) +} + +fn upload(paste: Data, key: Option) -> io::Result { + let (filename, url) = new_paste(); + + paste.stream_to_file(Path::new(&filename))?; + Ok(url) +} + +fn upload_string(paste: &str, key: Option) -> io::Result { + let (filename, url) = new_paste(); + + fs::write(filename, paste).expect("Unable to write file"); + Ok(url) +} + +#[derive(FromForm)] +struct PasteForm { + content: String, + extension: String, +} + +// todo: change /w to /, shouldn't conflict because of format, but it does currently +#[post("/w", format = "application/x-www-form-urlencoded", data = "")] +fn web_post(paste: LenientForm) -> io::Result { + upload_string(&paste.get().content, None) +} + +// todo: change /w to /, shouldn't conflict because of format, but it does currently +#[post("/m", format = "multipart/form-data", data = "")] +fn mpu_post(paste: MultipartUpload) -> io::Result { + let (filename, url) = new_paste(); + + paste.stream_to_file(Path::new(&filename))?; + Ok(url) +} + +#[put("/", data = "")] +fn upload_put(paste: Data) -> io::Result { + upload(paste, None) +} + +#[post("/", data = "")] +fn upload_post(paste: Data) -> io::Result { + upload(paste, None) +} + +#[patch("/", data = "")] +fn upload_patch(paste: Data) -> io::Result { + upload(paste, None) +} + +#[put("/", data = "")] +fn upload_put_key(paste: Data, key: PasteID) -> io::Result { + upload(paste, Some(key)) +} + +#[post("/", data = "")] +fn upload_post_key(paste: Data, key: PasteID) -> io::Result { + upload(paste, Some(key)) +} + +#[patch("/", data = "")] +fn upload_patch_key(paste: Data, key: PasteID) -> io::Result { + upload(paste, Some(key)) +} + +#[get("/")] +fn get(id: PasteID) -> Option> { + let filename = format!("upload/{id}", id = id); + File::open(&filename).map(|f| content::Plain(f)).ok() +} + +#[delete("//")] +fn delete(id: PasteID, key: PasteID) -> Option> { + let filename = format!("upload/{id}", id = id); + File::open(&filename).map(|f| content::Plain(f)).ok() +} + +#[patch("//")] +fn patch(id: PasteID, key: PasteID) -> Option> { + let filename = format!("upload/{id}", id = id); + File::open(&filename).map(|f| content::Plain(f)).ok() +} + +#[get("/")] +fn index() -> io::Result { + NamedFile::open("static/index.html") +} + +#[get("/static/")] +fn files(file: PathBuf) -> Option { + NamedFile::open(Path::new("static/").join(file)).ok() +} + +fn rocket() -> rocket::Rocket { + rocket::ignite().mount("/", routes![ + index, files, + web_post, + mpu_post, + upload_post, upload_put, upload_patch, + upload_post_key, upload_put_key, upload_patch_key, + get, delete, patch + ]) +} + +// adapted from io::copy +fn copy(reader: &mut R, writer: &mut W) -> io::Result + where R: Read, W: Write +{ + let mut buf : [u8; 8192] = [0; 8192]; + + let mut written = 0; + loop { + let len = match reader.read(&mut buf) { + Ok(0) => return Ok(written), + Ok(len) => len, + Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + }; + writer.write_all(&buf[..len])?; + written += len as u64; + if written > UPLOAD_MAX_SIZE { + return Err(std::io::Error::from(std::io::ErrorKind::InvalidData)) + } + } +} + +fn run_tcp(){ + // Bind the server's socket + thread::spawn(|| { + let timeout = Some(Duration::new(5, 0)); + let mut listener = TcpListener::bind("127.0.0.1:12345").unwrap(); + + loop { + match listener.accept() { + Ok((mut stream, addr)) => { + thread::spawn(move || { + let (filename, url) = new_paste(); + + let mut paste_file = File::create(&filename).expect("cannot create file?"); + + stream.set_read_timeout(timeout).expect("set read timeout failed?"); + stream.set_write_timeout(timeout).expect("set write timeout failed?"); + + stream.write(&url.into_bytes()).expect("write failed?"); + stream.flush(); + + copy(&mut stream, &mut paste_file); + + //handle_request(stream, addr); + }) + }, + Err(e) => { + thread::spawn(move || { + println!("Connection failed: {:?}", e) + }) + }, + }; + }; + }); + /* + // Bind the server's socket + let addr = "127.0.0.1:12345".parse().unwrap(); + let tcp = TcpListener::bind(&addr).unwrap(); + + // Iterate incoming connections + let server = tcp.incoming().for_each(|tcp| { + + let id = PasteID::new(ID_LENGTH); + let filename = format!("upload/{id}", id = id); + let url = format!("{host}/{id}\n", host = HOST, id = id); + + // Split up the read and write halves + //let (reader, writer) = tcp.split(); + + + // Copy the data back to the client + let conn = tokio::io::copy(reader, writer) + // print what happened + .map(|(n, _, _)| { + println!("wrote {} bytes", n) + }) + // Handle any errors + .map_err(|err| { + println!("IO error {:?}", err) + }); + + let conn = tokio::io::write_all(writer, url) + .then(|res| { + println!("wrote message; success={:?}", res.is_ok()); + Ok(()) + }); + + + let mut paste_file = File::create(&filename)?; + + let conn = tokio::io::re(reader, vec!(0; 4096)).then(move |res| { + let result = match res { + Ok((_, buf, n)) => { + //info!(client_logger, "persisted"; "filepath" => filepath); + paste_file.write(&buf[0..n]).unwrap(); + + //info!(client_logger, "replied"; "message" => url); + tokio::io::write_all(writer, format!("{}\n", url).as_bytes()).wait().unwrap(); + + //info!(client_logger, "finished connection"); + Ok(()) + } + Err(e) => { + //error!(client_logger, "failed to read from client"); + Err(e) + } + }; + drop(result); + Ok(()) + }); + + let mut paste_file = tokio::fs::file::File::create(&filename); + + let conn = tokio::io::copy(reader, paste_file) + // print what happened + .map(|(n, _, _)| { + println!("wrote {} bytes", n) + }) + // Handle any errors + .map_err(|err| { + println!("IO error {:?}", err) + }); + + + + let framed = BytesCodec::new().framed(tcp); + let (writer, reader) = framed.split(); + + let mut paste_file = File::create(&filename)?; + + let conn = writer.write_buf(url) + .then(|res| { + println!("wrote message; success={:?}", res.is_ok()); + Ok(()) + }); + + let conn = reader + .for_each(move|bytes| { + println!("bytes: {:?}", bytes); + paste_file.write_all(&bytes).expect("cannot write to file?"); + Ok(()) + }) + // After our copy operation is complete we just print out some helpful + // information. + .and_then(|()| { + println!("Socket received FIN packet and closed connection"); + Ok(()) + }) + .or_else(|err| { + println!("Socket closed with error: {:?}", err); + // We have to return the error to catch it in the next ``.then` call + Err(err) + }) + .then(|result| { + println!("Socket closed with result: {:?}", result); + Ok(()) + }); + + // Spawn the future as a concurrent task + tokio::spawn(conn); + + Ok(()) + }) + .map_err(|err| { + println!("server error {:?}", err); + }); + + // Start the runtime and spin up the server + tokio::run(server); + */ +} + +fn main() { + + run_tcp(); + rocket().launch(); +} diff --git a/src/mpu.rs b/src/mpu.rs new file mode 100644 index 0000000..0d084f6 --- /dev/null +++ b/src/mpu.rs @@ -0,0 +1,63 @@ +use std::io::prelude::*; +use std::fs::File; +use std::path::Path; +use std::io; + +use std::io::{Read, Cursor}; +use rocket::data::{self, FromData}; +use rocket::Data; +use rocket::Outcome; +use rocket::request::Request; +use rocket::http::Status; +use multipart::server::Multipart; + +pub struct MultipartUpload { + file: Vec, +} + +impl MultipartUpload { + pub fn stream_to_file(&self, path: &Path) -> io::Result<()> { + let mut f = File::create(path)?; + f.write_all(&self.file) + } +} + +fn get_multipart(request: &Request, data: Data) -> Option>>> { + // All of these errors should be reported + let ct = request.headers().get_one("Content-Type")?; + let idx = ct.find("boundary=")?; + let boundary = &ct[(idx + "boundary=".len())..]; + + let mut d = Vec::new(); + data.stream_to(&mut d).ok()?; + + Some(Multipart::with_body(Cursor::new(d), boundary)) +} + +impl FromData for MultipartUpload { + type Error = (); + + fn from_data(request: &Request, data: Data) -> data::Outcome { + let mut mp = match get_multipart(&request, data) { + Some(m) => m, + None => return Outcome::Failure((Status::raw(421), ())), + }; + + // Custom implementation parts + let mut file = None; + + mp.foreach_entry(|mut entry| match entry.headers.name.as_str() { + "file" => { + panic!("filename: {:?}", entry.headers.filename); + let mut d = Vec::new(); + entry.data.read_to_end(&mut d).expect("cant read"); + file = Some(d); + } + other => panic!("No known key {}", other), + }).expect("Unable to iterate"); + + Outcome::Success(MultipartUpload { + file: file.expect("file not set"), + }) + } +} diff --git a/src/paste_id.rs b/src/paste_id.rs new file mode 100644 index 0000000..85c17c7 --- /dev/null +++ b/src/paste_id.rs @@ -0,0 +1,51 @@ +use std::fmt; +use std::borrow::Cow; + +use rocket::request::FromParam; +use rocket::http::RawStr; + +use adjective_adjective_animal::Generator; + +/// A _probably_ unique paste ID. +pub struct PasteID<'a>(Cow<'a, str>); + +impl<'a> PasteID<'a> { + /// Generate a _probably_ unique ID with `size` characters. For readability, + /// the characters used are from the sets [0-9], [A-Z], [a-z]. The + /// probability of a collision depends on the value of `size` and the number + /// of IDs generated thus far. + pub fn new() -> PasteID<'static> { + let mut generator = Generator::default(); + + PasteID(Cow::Owned(generator.next().unwrap())) + } +} + +impl<'a> fmt::Display for PasteID<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Returns `true` if `id` is a valid paste ID and `false` otherwise. +fn valid_id(id: &str) -> bool { + id.len() > 3 && + id.chars().all(|c| { + (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + }) +} + +/// Returns an instance of `PasteID` if the path segment is a valid ID. +/// Otherwise returns the invalid ID as the `Err` value. +impl<'a> FromParam<'a> for PasteID<'a> { + type Error = &'a RawStr; + + fn from_param(param: &'a RawStr) -> Result, &'a RawStr> { + match valid_id(param) { + true => Ok(PasteID(Cow::Borrowed(param))), + false => Err(param) + } + } +} + diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..487ad96 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,70 @@ +use super::{rocket, index}; +use rocket::local::Client; +use rocket::http::{Status, ContentType}; + +fn extract_id(from: &str) -> Option { + from.rfind('/').map(|i| &from[(i + 1)..]).map(|s| s.trim_right().to_string()) +} + +#[test] +fn check_index() { + let client = Client::new(rocket()).unwrap(); + + // Ensure the index returns what we expect. + let mut response = client.get("/").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::Plain)); + //assert_eq!(response.body_string(), Some(index().into())) +} + +fn upload_paste(client: &Client, body: &str) -> String { + let mut response = client.post("/").body(body).dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.content_type(), Some(ContentType::Plain)); + extract_id(&response.body_string().unwrap()).unwrap() +} + +fn download_paste(client: &Client, id: &str) -> String { + let mut response = client.get(format!("/{}", id)).dispatch(); + assert_eq!(response.status(), Status::Ok); + response.body_string().unwrap() +} + +#[test] +fn pasting() { + let client = Client::new(rocket()).unwrap(); + + // Do a trivial upload, just to make sure it works. + let body_1 = "Hello, world!"; + let id_1 = upload_paste(&client, body_1); + assert_eq!(download_paste(&client, &id_1), body_1); + + // Make sure we can keep getting that paste. + assert_eq!(download_paste(&client, &id_1), body_1); + assert_eq!(download_paste(&client, &id_1), body_1); + assert_eq!(download_paste(&client, &id_1), body_1); + + // Upload some unicode. + let body_2 = "こんにちは"; + let id_2 = upload_paste(&client, body_2); + assert_eq!(download_paste(&client, &id_2), body_2); + + // Make sure we can get both pastes. + assert_eq!(download_paste(&client, &id_1), body_1); + assert_eq!(download_paste(&client, &id_2), body_2); + assert_eq!(download_paste(&client, &id_1), body_1); + assert_eq!(download_paste(&client, &id_2), body_2); + + // Now a longer upload. + let body_3 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed + do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim + ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit + in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui + officia deserunt mollit anim id est laborum."; + let id_3 = upload_paste(&client, body_3); + assert_eq!(download_paste(&client, &id_3), body_3); + assert_eq!(download_paste(&client, &id_1), body_1); + assert_eq!(download_paste(&client, &id_2), body_2); +} diff --git a/static/api.txt b/static/api.txt new file mode 100644 index 0000000..40603f0 --- /dev/null +++ b/static/api.txt @@ -0,0 +1,12 @@ + USAGE + + POST / + + accepts raw data in the body of the request and responds with a URL of + a page containing the body's content + + EXMAPLE: curl --data-binary @file.txt http://localhost:8000 + + GET / + + retrieves the content for the paste with id `` \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..13de010 --- /dev/null +++ b/static/index.html @@ -0,0 +1,875 @@ + + + + + Rust Bucket + + + +
+ +
+

+ Select a file:
+ +

+
+ +
+
+ + +
+ +
+ + + +
+ +
+
+ +