use crate::auth::{extract_tokens, handle_unauthorized_error, unauthorized_error}; use crate::config::{Config, LandingPageConfig, TokenType}; use crate::file::Directory; use crate::header::{self, ContentDisposition}; use crate::mime as mime_util; use crate::paste::{Paste, PasteType}; use crate::util::{self, safe_path_join}; use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::http::StatusCode; use actix_web::middleware::ErrorHandlers; use actix_web::{delete, error, get, post, web, Error, HttpRequest, HttpResponse}; use actix_web_grants::GrantsMiddleware; use awc::Client; use byte_unit::{Byte, UnitType}; use futures_util::stream::StreamExt; use mime::TEXT_PLAIN_UTF_8; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::env; use std::fs; use std::path::PathBuf; use std::sync::RwLock; use std::time::Duration; use uts2ts; /// Shows the landing page. #[get("/")] #[allow(deprecated)] async fn index(config: web::Data>) -> Result { let mut config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? .clone(); let redirect = HttpResponse::Found() .append_header(("Location", env!("CARGO_PKG_HOMEPAGE"))) .finish(); if config.server.landing_page.is_some() { if config.landing_page.is_none() { config.landing_page = Some(LandingPageConfig::default()); } if let Some(ref mut landing_page) = config.landing_page { landing_page.text = config.server.landing_page; } } if config.server.landing_page_content_type.is_some() { if config.landing_page.is_none() { config.landing_page = Some(LandingPageConfig::default()); } if let Some(ref mut landing_page) = config.landing_page { landing_page.content_type = config.server.landing_page_content_type; } } if let Some(mut landing_page) = config.landing_page { if let Some(file) = landing_page.file { landing_page.text = fs::read_to_string(file).ok(); } match landing_page.text { Some(page) => Ok(HttpResponse::Ok() .content_type( landing_page .content_type .unwrap_or(TEXT_PLAIN_UTF_8.to_string()), ) .body(page)), None => Ok(redirect), } } else { Ok(redirect) } } /// File serving options (i.e. query parameters). #[derive(Debug, Deserialize)] struct ServeOptions { /// If set to `true`, change the MIME type to `application/octet-stream` and force downloading /// the file. download: bool, } /// Serves a file from the upload directory. #[get("/{file}")] async fn serve( request: HttpRequest, file: web::Path, options: Option>, config: web::Data>, ) -> Result { let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?; let mut paste_type = PasteType::File; if !path.exists() || path.is_dir() { for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] { let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?; let alt_path = util::glob_match_file(alt_path)?; if alt_path.exists() || path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir()) { path = alt_path; paste_type = *type_; break; } } } if !path.is_file() || !path.exists() { return Err(error::ErrorNotFound("file is not found or expired :(\n")); } match paste_type { PasteType::File | PasteType::RemoteFile | PasteType::Oneshot => { let mime_type = if options.map(|v| v.download).unwrap_or(false) { mime::APPLICATION_OCTET_STREAM } else { mime_util::get_mime_type(&config.paste.mime_override, file.to_string()) .map_err(error::ErrorInternalServerError)? }; let response = NamedFile::open(&path)? .disable_content_disposition() .set_content_type(mime_type) .prefer_utf8(true) .into_response(&request); if paste_type.is_oneshot() { fs::rename( &path, path.with_file_name(format!( "{}.{}", file, util::get_system_time()?.as_millis() )), )?; } Ok(response) } PasteType::Url => Ok(HttpResponse::Found() .append_header(("Location", fs::read_to_string(&path)?)) .finish()), PasteType::OneshotUrl => { let resp = HttpResponse::Found() .append_header(("Location", fs::read_to_string(&path)?)) .finish(); fs::rename( &path, path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())), )?; Ok(resp) } } } /// Remove a file from the upload directory. #[delete("/{file}")] #[actix_web_grants::protect("TokenType::Delete", ty = TokenType, error = unauthorized_error)] async fn delete( file: web::Path, config: web::Data>, ) -> Result { let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?; if !path.is_file() || !path.exists() { return Err(error::ErrorNotFound("file is not found or expired :(\n")); } match fs::remove_file(path) { Ok(_) => info!("deleted file: {:?}", file.to_string()), Err(e) => { error!("cannot delete file: {}", e); return Err(error::ErrorInternalServerError("cannot delete file")); } } Ok(HttpResponse::Ok().body(String::from("file deleted\n"))) } /// Expose version endpoint #[get("/version")] #[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)] async fn version(config: web::Data>) -> Result { let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; if !config.server.expose_version.unwrap_or(false) { warn!("server is not configured to expose version endpoint"); Err(error::ErrorNotFound(""))?; } let version = env!("CARGO_PKG_VERSION"); Ok(HttpResponse::Ok().body(version.to_owned() + "\n")) } /// Handles file upload by processing `multipart/form-data`. #[post("/")] #[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)] async fn upload( request: HttpRequest, mut payload: Multipart, client: web::Data, config: web::Data>, ) -> Result { let connection = request.connection_info().clone(); let host = connection.realip_remote_addr().unwrap_or("unknown host"); let server_url = match config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? .server .url .clone() { Some(v) => v, None => { format!("{}://{}", connection.scheme(), connection.host(),) } }; let time = util::get_system_time()?; let mut expiry_date = header::parse_expiry_date(request.headers(), time)?; if expiry_date.is_none() { expiry_date = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? .paste .default_expiry .and_then(|v| time.checked_add(v).map(|t| t.as_millis())); } let mut urls: Vec = Vec::new(); while let Some(item) = payload.next().await { let header_filename = header::parse_header_filename(request.headers())?; let mut field = item?; let content = ContentDisposition::from(field.content_disposition().clone()); if let Ok(paste_type) = PasteType::try_from(&content) { let mut bytes = Vec::::new(); while let Some(chunk) = field.next().await { bytes.append(&mut chunk?.to_vec()); } if bytes.is_empty() { warn!("{} sent zero bytes", host); return Err(error::ErrorBadRequest("invalid file size")); } if paste_type != PasteType::Oneshot && paste_type != PasteType::RemoteFile && paste_type != PasteType::OneshotUrl && expiry_date.is_none() && !config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? .paste .duplicate_files .unwrap_or(true) { let bytes_checksum = util::sha256_digest(&*bytes)?; let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; if let Some(file) = Directory::try_from(config.server.upload_path.as_path())? .get_file(bytes_checksum) { urls.push(format!( "{}/{}\n", server_url, file.path .file_name() .map(|v| v.to_string_lossy()) .unwrap_or_default() )); continue; } } let mut paste = Paste { data: bytes.to_vec(), type_: paste_type, }; let mut file_name = match paste.type_ { PasteType::File | PasteType::Oneshot => { let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; paste.store_file( content.get_file_name()?, expiry_date, header_filename, &config, )? } PasteType::RemoteFile => { paste .store_remote_file(expiry_date, &client, &config) .await? } PasteType::Url | PasteType::OneshotUrl => { let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; paste.store_url(expiry_date, &config)? } }; info!( "{} ({}) is uploaded from {}", file_name, Byte::from_u128(paste.data.len() as u128) .unwrap_or_default() .get_appropriate_unit(UnitType::Decimal), host ); let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; if let Some(handle_spaces_config) = config.server.handle_spaces { file_name = handle_spaces_config.process_filename(&file_name); } urls.push(format!("{}/{}\n", server_url, file_name)); } else { warn!("{} sent an invalid form field", host); return Err(error::ErrorBadRequest("invalid form field")); } } Ok(HttpResponse::Ok().body(urls.join(""))) } /// File entry item for list endpoint. #[derive(Serialize, Deserialize)] pub struct ListItem { /// Uploaded file name. pub file_name: PathBuf, /// Size of the file in bytes. pub file_size: u64, /// ISO8601 formatted date-time string of the expiration timestamp if one exists for this file. pub expires_at_utc: Option, } /// Returns the list of files. #[get("/list")] #[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)] async fn list(config: web::Data>) -> Result { let config = config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? .clone(); if !config.server.expose_list.unwrap_or(false) { warn!("server is not configured to expose list endpoint"); Err(error::ErrorNotFound(""))?; } let entries: Vec = fs::read_dir(config.server.upload_path)? .filter_map(|entry| { entry.ok().and_then(|e| { let metadata = match e.metadata() { Ok(metadata) => { if metadata.is_dir() { return None; } metadata } Err(e) => { error!("failed to read metadata: {e}"); return None; } }; let mut file_name = PathBuf::from(e.file_name()); let expires_at_utc = if let Some(expiration) = file_name .extension() .and_then(|ext| ext.to_str()) .and_then(|v| v.parse::().ok()) { file_name.set_extension(""); if util::get_system_time().ok()? > Duration::from_millis(expiration.try_into().ok()?) { return None; } Some(uts2ts::uts2ts(expiration / 1000).as_string()) } else { None }; Some(ListItem { file_name, file_size: metadata.len(), expires_at_utc, }) }) }) .collect(); Ok(HttpResponse::Ok().json(entries)) } /// Configures the server routes. pub fn configure_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("") .service(index) .service(version) .service(list) .service(serve) .service(upload) .service(delete) .route("", web::head().to(HttpResponse::MethodNotAllowed)) .wrap(GrantsMiddleware::with_extractor(extract_tokens)) .wrap( ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized_error), ), ); } #[cfg(test)] mod tests { use super::*; use crate::config::LandingPageConfig; use crate::middleware::ContentLengthLimiter; use crate::random::{RandomURLConfig, RandomURLType}; use actix_web::body::MessageBody; use actix_web::body::{BodySize, BoxBody}; use actix_web::error::Error; use actix_web::http::header::AUTHORIZATION; use actix_web::http::{header, StatusCode}; use actix_web::test::{self, TestRequest}; use actix_web::web::Data; use actix_web::App; use awc::ClientBuilder; use glob::glob; use std::fs::File; use std::io::Write; use std::path::PathBuf; use std::str; use std::thread; use std::time::Duration; fn get_multipart_request(data: &str, name: &str, filename: &str) -> TestRequest { let multipart_data = format!( "\r\n\ --multipart_bound\r\n\ Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\n\r\n\ {}\r\n\ --multipart_bound--\r\n", name, filename, data.bytes().len(), data, ); TestRequest::post() .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("multipart/mixed; boundary=\"multipart_bound\""), )) .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_str(&data.bytes().len().to_string()) .expect("cannot create header value"), )) .set_payload(multipart_data) } async fn assert_body(body: BoxBody, expected: &str) -> Result<(), Error> { if let BodySize::Sized(size) = body.size() { assert_eq!(size, expected.as_bytes().len() as u64); let body_bytes = actix_web::body::to_bytes(body).await?; let body_text = str::from_utf8(&body_bytes)?; assert_eq!(expected, body_text); Ok(()) } else { Err(error::ErrorInternalServerError("unexpected body type")) } } #[actix_web::test] async fn test_index() { let config = Config::default(); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .service(index), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::FOUND, response.status()); } #[actix_web::test] async fn test_index_with_landing_page() -> Result<(), Error> { let config = Config { landing_page: Some(LandingPageConfig { text: Some(String::from("landing page")), ..Default::default() }), ..Default::default() }; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .service(index), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), "landing page").await?; Ok(()) } #[actix_web::test] async fn test_index_with_landing_page_file() -> Result<(), Error> { let filename = "landing_page.txt"; let config = Config { landing_page: Some(LandingPageConfig { file: Some(filename.to_string()), ..Default::default() }), ..Default::default() }; let mut file = File::create(filename)?; file.write_all("landing page from file".as_bytes())?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .service(index), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), "landing page from file").await?; fs::remove_file(filename)?; Ok(()) } #[actix_web::test] async fn test_index_with_landing_page_file_not_found() -> Result<(), Error> { let filename = "landing_page.txt"; let config = Config { landing_page: Some(LandingPageConfig { text: Some(String::from("landing page")), file: Some(filename.to_string()), ..Default::default() }), ..Default::default() }; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .service(index), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::FOUND, response.status()); Ok(()) } #[actix_web::test] async fn test_version_without_auth() -> Result<(), Error> { let mut config = Config::default(); config.server.auth_tokens = Some(["test".to_string()].into()); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .uri("/version") .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::UNAUTHORIZED, response.status()); assert_body(response.into_body(), "unauthorized\n").await?; Ok(()) } #[actix_web::test] async fn test_version_without_config() -> Result<(), Error> { let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(Config::default()))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .uri("/version") .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); assert_body(response.into_body(), "").await?; Ok(()) } #[actix_web::test] async fn test_version() -> Result<(), Error> { let mut config = Config::default(); config.server.expose_version = Some(true); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .uri("/version") .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body(), &(env!("CARGO_PKG_VERSION").to_owned() + "\n"), ) .await?; Ok(()) } #[actix_web::test] async fn test_list() -> Result<(), Error> { let mut config = Config::default(); config.server.expose_list = Some(true); let test_upload_dir = "test_upload"; fs::create_dir(test_upload_dir)?; config.server.upload_path = PathBuf::from(test_upload_dir); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let filename = "test_file.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); test::call_service( &app, get_multipart_request(×tamp, "file", filename).to_request(), ) .await; let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .uri("/list") .to_request(); let result: Vec = test::call_and_read_body_json(&app, request).await; assert_eq!(result.len(), 1); assert_eq!( result.first().expect("json object").file_name, PathBuf::from(filename) ); fs::remove_dir_all(test_upload_dir)?; Ok(()) } #[actix_web::test] async fn test_list_expired() -> Result<(), Error> { let mut config = Config::default(); config.server.expose_list = Some(true); let test_upload_dir = "test_upload"; fs::create_dir(test_upload_dir)?; config.server.upload_path = PathBuf::from(test_upload_dir); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let filename = "test_file.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); test::call_service( &app, get_multipart_request(×tamp, "file", filename) .insert_header(( header::HeaderName::from_static("expire"), header::HeaderValue::from_static("50ms"), )) .to_request(), ) .await; thread::sleep(Duration::from_millis(500)); let request = TestRequest::default() .insert_header(("content-type", "text/plain")) .uri("/list") .to_request(); let result: Vec = test::call_and_read_body_json(&app, request).await; assert!(result.is_empty()); fs::remove_dir_all(test_upload_dir)?; Ok(()) } #[actix_web::test] async fn test_auth() -> Result<(), Error> { let mut config = Config::default(); config.server.auth_tokens = Some(["test".to_string()].into()); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let response = test::call_service(&app, get_multipart_request("", "", "").to_request()).await; assert_eq!(StatusCode::UNAUTHORIZED, response.status()); assert_body(response.into_body(), "unauthorized\n").await?; Ok(()) } #[actix_web::test] async fn test_payload_limit() -> Result<(), Error> { let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(Config::default()))) .app_data(Data::new(Client::default())) .wrap(ContentLengthLimiter::new(Byte::from_u64(1))) .configure(configure_routes), ) .await; let response = test::call_service( &app, get_multipart_request("test", "file", "test").to_request(), ) .await; assert_eq!(StatusCode::PAYLOAD_TOO_LARGE, response.status()); assert_body(response.into_body().boxed(), "upload limit exceeded").await?; Ok(()) } #[actix_web::test] async fn test_delete_file() -> Result<(), Error> { let mut config = Config::default(); config.server.delete_tokens = Some(["test".to_string()].into()); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let file_name = "test_file.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); test::call_service( &app, get_multipart_request(×tamp, "file", file_name).to_request(), ) .await; let request = TestRequest::delete() .insert_header((AUTHORIZATION, header::HeaderValue::from_static("test"))) .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), "file deleted\n").await?; let path = PathBuf::from(file_name); assert!(!path.exists()); Ok(()) } #[actix_web::test] async fn test_delete_file_without_token_in_config() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let file_name = "test_file.txt"; let request = TestRequest::delete() .insert_header((AUTHORIZATION, header::HeaderValue::from_static("test"))) .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); assert_body(response.into_body(), "").await?; Ok(()) } #[actix_web::test] async fn test_upload_file() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let file_name = "test_file.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); let response = test::call_service( &app, get_multipart_request(×tamp, "file", file_name).to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body(), &format!("http://localhost:8080/{file_name}\n"), ) .await?; let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), ×tamp).await?; fs::remove_file(file_name)?; let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); Ok(()) } #[actix_web::test] async fn test_upload_file_override_filename() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let file_name = "test_file.txt"; let header_filename = "fn_from_header.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); let response = test::call_service( &app, get_multipart_request(×tamp, "file", file_name) .insert_header(( header::HeaderName::from_static("filename"), header::HeaderValue::from_static("fn_from_header.txt"), )) .to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body(), &format!("http://localhost:8080/{header_filename}\n"), ) .await?; let serve_request = TestRequest::get() .uri(&format!("/{header_filename}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), ×tamp).await?; fs::remove_file(header_filename)?; let serve_request = TestRequest::get() .uri(&format!("/{header_filename}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); Ok(()) } #[actix_web::test] async fn test_upload_same_filename() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let file_name = "test_file.txt"; let header_filename = "fn_from_header.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); let response = test::call_service( &app, get_multipart_request(×tamp, "file", file_name) .insert_header(( header::HeaderName::from_static("filename"), header::HeaderValue::from_static("fn_from_header.txt"), )) .to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body(), &format!("http://localhost:8080/{header_filename}\n"), ) .await?; let timestamp = util::get_system_time()?.as_secs().to_string(); let response = test::call_service( &app, get_multipart_request(×tamp, "file", file_name) .insert_header(( header::HeaderName::from_static("filename"), header::HeaderValue::from_static("fn_from_header.txt"), )) .to_request(), ) .await; assert_eq!(StatusCode::CONFLICT, response.status()); assert_body(response.into_body(), "file already exists\n").await?; fs::remove_file(header_filename)?; Ok(()) } #[actix_web::test] #[allow(deprecated)] async fn test_upload_duplicate_file() -> Result<(), Error> { let test_upload_dir = "test_upload"; fs::create_dir(test_upload_dir)?; let mut config = Config::default(); config.server.upload_path = PathBuf::from(&test_upload_dir); config.paste.duplicate_files = Some(false); config.paste.random_url = Some(RandomURLConfig { enabled: Some(true), type_: RandomURLType::Alphanumeric, ..Default::default() }); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let response = test::call_service( &app, get_multipart_request("test", "file", "x").to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); let body = response.into_body(); let first_body_bytes = actix_web::body::to_bytes(body).await?; let response = test::call_service( &app, get_multipart_request("test", "file", "x").to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); let body = response.into_body(); let second_body_bytes = actix_web::body::to_bytes(body).await?; assert_eq!(first_body_bytes, second_body_bytes); fs::remove_dir_all(test_upload_dir)?; Ok(()) } #[actix_web::test] async fn test_upload_expiring_file() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let file_name = "test_file.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); let response = test::call_service( &app, get_multipart_request(×tamp, "file", file_name) .insert_header(( header::HeaderName::from_static("expire"), header::HeaderValue::from_static("20ms"), )) .to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body(), &format!("http://localhost:8080/{file_name}\n"), ) .await?; let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), ×tamp).await?; thread::sleep(Duration::from_millis(40)); let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); if let Some(glob_path) = glob(&format!("{file_name}.[0-9]*")) .map_err(error::ErrorInternalServerError)? .next() { fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?; } Ok(()) } #[actix_web::test] async fn test_upload_remote_file() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default(); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) .app_data(Data::new( ClientBuilder::new() .timeout(Duration::from_secs(30)) .finish(), )) .configure(configure_routes), ) .await; let file_name = "Example.jpg"; let response = test::call_service( &app, get_multipart_request( "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg", "remote", file_name, ) .to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body().boxed(), &format!("http://localhost:8080/{file_name}\n"), ) .await?; let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::OK, response.status()); let body = response.into_body(); let body_bytes = actix_web::body::to_bytes(body).await?; assert_eq!( "70ff72a2f7651b5fae3aa9834e03d2a2233c52036610562f7fa04e089e8198ed", util::sha256_digest(&*body_bytes)? ); fs::remove_file(file_name)?; let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); Ok(()) } #[actix_web::test] async fn test_upload_url() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config.clone()))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let url_upload_path = PasteType::Url .get_path(&config.server.upload_path) .expect("Bad upload path"); fs::create_dir_all(&url_upload_path)?; let response = test::call_service( &app, get_multipart_request(env!("CARGO_PKG_HOMEPAGE"), "url", "").to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), "http://localhost:8080/url\n").await?; let serve_request = TestRequest::get().uri("/url").to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::FOUND, response.status()); fs::remove_file(url_upload_path.join("url"))?; fs::remove_dir(url_upload_path)?; let serve_request = TestRequest::get().uri("/url").to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); Ok(()) } #[actix_web::test] async fn test_upload_oneshot() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config.clone()))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let oneshot_upload_path = PasteType::Oneshot .get_path(&config.server.upload_path) .expect("Bad upload path"); fs::create_dir_all(&oneshot_upload_path)?; let file_name = "oneshot.txt"; let timestamp = util::get_system_time()?.as_secs().to_string(); let response = test::call_service( &app, get_multipart_request(×tamp, "oneshot", file_name).to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body(), &format!("http://localhost:8080/{file_name}\n"), ) .await?; let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::OK, response.status()); assert_body(response.into_body(), ×tamp).await?; let serve_request = TestRequest::get() .uri(&format!("/{file_name}")) .to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); if let Some(glob_path) = glob( &oneshot_upload_path .join(format!("{file_name}.[0-9]*")) .to_string_lossy(), ) .map_err(error::ErrorInternalServerError)? .next() { fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?; } fs::remove_dir(oneshot_upload_path)?; Ok(()) } #[actix_web::test] async fn test_upload_oneshot_url() -> Result<(), Error> { let mut config = Config::default(); config.server.upload_path = env::current_dir()?; let oneshot_url_suffix = "oneshot_url"; let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config.clone()))) .app_data(Data::new(Client::default())) .configure(configure_routes), ) .await; let url_upload_path = PasteType::OneshotUrl .get_path(&config.server.upload_path) .expect("Bad upload path"); fs::create_dir_all(&url_upload_path)?; let response = test::call_service( &app, get_multipart_request( env!("CARGO_PKG_HOMEPAGE"), oneshot_url_suffix, oneshot_url_suffix, ) .to_request(), ) .await; assert_eq!(StatusCode::OK, response.status()); assert_body( response.into_body(), &format!("http://localhost:8080/{}\n", oneshot_url_suffix), ) .await?; // Make the oneshot_url request, ensure it is found. let serve_request = TestRequest::with_uri(&format!("/{}", oneshot_url_suffix)).to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::FOUND, response.status()); // Make the same request again, and ensure that the oneshot_url is not found. let serve_request = TestRequest::with_uri(&format!("/{}", oneshot_url_suffix)).to_request(); let response = test::call_service(&app, serve_request).await; assert_eq!(StatusCode::NOT_FOUND, response.status()); // Cleanup fs::remove_dir_all(url_upload_path)?; Ok(()) } }