rustypaste/
server.rs

1use crate::auth::{extract_tokens, handle_unauthorized_error, unauthorized_error};
2use crate::config::{Config, LandingPageConfig, TokenType};
3use crate::file::Directory;
4use crate::header::{self, ContentDisposition};
5use crate::mime as mime_util;
6use crate::paste::{Paste, PasteType};
7use crate::util::{self, safe_path_join};
8use actix_files::NamedFile;
9use actix_multipart::Multipart;
10use actix_web::http::StatusCode;
11use actix_web::middleware::ErrorHandlers;
12use actix_web::{delete, error, get, post, web, Error, HttpRequest, HttpResponse};
13use actix_web_grants::GrantsMiddleware;
14use awc::Client;
15use byte_unit::{Byte, UnitType};
16use futures_util::stream::StreamExt;
17use mime::TEXT_PLAIN_UTF_8;
18use serde::{Deserialize, Serialize};
19use std::convert::TryFrom;
20use std::env;
21use std::fs;
22use std::path::PathBuf;
23use std::sync::RwLock;
24use std::time::{Duration, UNIX_EPOCH};
25use uts2ts;
26
27/// Shows the landing page.
28#[get("/")]
29#[allow(deprecated)]
30async fn index(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
31    let mut config = config
32        .read()
33        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
34        .clone();
35    let redirect = HttpResponse::Found()
36        .append_header(("Location", env!("CARGO_PKG_HOMEPAGE")))
37        .finish();
38    if config.server.landing_page.is_some() {
39        if config.landing_page.is_none() {
40            config.landing_page = Some(LandingPageConfig::default());
41        }
42        if let Some(ref mut landing_page) = config.landing_page {
43            landing_page.text = config.server.landing_page;
44        }
45    }
46    if config.server.landing_page_content_type.is_some() {
47        if config.landing_page.is_none() {
48            config.landing_page = Some(LandingPageConfig::default());
49        }
50        if let Some(ref mut landing_page) = config.landing_page {
51            landing_page.content_type = config.server.landing_page_content_type;
52        }
53    }
54    if let Some(mut landing_page) = config.landing_page {
55        if let Some(file) = landing_page.file {
56            landing_page.text = fs::read_to_string(file).ok();
57        }
58        match landing_page.text {
59            Some(page) => Ok(HttpResponse::Ok()
60                .content_type(
61                    landing_page
62                        .content_type
63                        .unwrap_or(TEXT_PLAIN_UTF_8.to_string()),
64                )
65                .body(page)),
66            None => Ok(redirect),
67        }
68    } else {
69        Ok(redirect)
70    }
71}
72
73/// File serving options (i.e. query parameters).
74#[derive(Debug, Deserialize)]
75struct ServeOptions {
76    /// If set to `true`, change the MIME type to `application/octet-stream` and force downloading
77    /// the file.
78    download: bool,
79}
80
81/// Serves a file from the upload directory.
82#[get("/{file}")]
83async fn serve(
84    request: HttpRequest,
85    file: web::Path<String>,
86    options: Option<web::Query<ServeOptions>>,
87    config: web::Data<RwLock<Config>>,
88) -> Result<HttpResponse, Error> {
89    let config = config
90        .read()
91        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
92    let mut path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
93    let mut paste_type = PasteType::File;
94    if !path.exists() || path.is_dir() {
95        for type_ in &[PasteType::Url, PasteType::Oneshot, PasteType::OneshotUrl] {
96            let alt_path = safe_path_join(type_.get_path(&config.server.upload_path)?, &*file)?;
97            let alt_path = util::glob_match_file(alt_path)?;
98            if alt_path.exists()
99                || path.file_name().and_then(|v| v.to_str()) == Some(&type_.get_dir())
100            {
101                path = alt_path;
102                paste_type = *type_;
103                break;
104            }
105        }
106    }
107    if !path.is_file() || !path.exists() {
108        return Err(error::ErrorNotFound("file is not found or expired :(\n"));
109    }
110    match paste_type {
111        PasteType::File | PasteType::RemoteFile | PasteType::Oneshot => {
112            let mime_type = if options.map(|v| v.download).unwrap_or(false) {
113                mime::APPLICATION_OCTET_STREAM
114            } else {
115                mime_util::get_mime_type(&config.paste.mime_override, file.to_string())
116                    .map_err(error::ErrorInternalServerError)?
117            };
118            let response = NamedFile::open(&path)?
119                .disable_content_disposition()
120                .set_content_type(mime_type)
121                .prefer_utf8(true)
122                .into_response(&request);
123            if paste_type.is_oneshot() {
124                fs::rename(
125                    &path,
126                    path.with_file_name(format!(
127                        "{}.{}",
128                        file,
129                        util::get_system_time()?.as_millis()
130                    )),
131                )?;
132            }
133            Ok(response)
134        }
135        PasteType::Url => Ok(HttpResponse::Found()
136            .append_header(("Location", fs::read_to_string(&path)?))
137            .finish()),
138        PasteType::OneshotUrl => {
139            let resp = HttpResponse::Found()
140                .append_header(("Location", fs::read_to_string(&path)?))
141                .finish();
142            fs::rename(
143                &path,
144                path.with_file_name(format!("{}.{}", file, util::get_system_time()?.as_millis())),
145            )?;
146            Ok(resp)
147        }
148    }
149}
150
151/// Remove a file from the upload directory.
152#[delete("/{file}")]
153#[actix_web_grants::protect("TokenType::Delete", ty = TokenType, error = unauthorized_error)]
154async fn delete(
155    file: web::Path<String>,
156    config: web::Data<RwLock<Config>>,
157) -> Result<HttpResponse, Error> {
158    let config = config
159        .read()
160        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
161    let path = util::glob_match_file(safe_path_join(&config.server.upload_path, &*file)?)?;
162    if !path.is_file() || !path.exists() {
163        return Err(error::ErrorNotFound("file is not found or expired :(\n"));
164    }
165    match fs::remove_file(path) {
166        Ok(_) => info!("deleted file: {:?}", file.to_string()),
167        Err(e) => {
168            error!("cannot delete file: {}", e);
169            return Err(error::ErrorInternalServerError("cannot delete file"));
170        }
171    }
172    Ok(HttpResponse::Ok().body(String::from("file deleted\n")))
173}
174
175/// Expose version endpoint
176#[get("/version")]
177#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
178async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
179    let config = config
180        .read()
181        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
182    if !config.server.expose_version.unwrap_or(false) {
183        warn!("server is not configured to expose version endpoint");
184        Err(error::ErrorNotFound(""))?;
185    }
186
187    let version = env!("CARGO_PKG_VERSION");
188    Ok(HttpResponse::Ok().body(version.to_owned() + "\n"))
189}
190
191/// Handles file upload by processing `multipart/form-data`.
192#[post("/")]
193#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
194async fn upload(
195    request: HttpRequest,
196    mut payload: Multipart,
197    client: web::Data<Client>,
198    config: web::Data<RwLock<Config>>,
199) -> Result<HttpResponse, Error> {
200    let connection = request.connection_info().clone();
201    let host = connection.realip_remote_addr().unwrap_or("unknown host");
202    let server_url = match config
203        .read()
204        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
205        .server
206        .url
207        .clone()
208    {
209        Some(v) => v,
210        None => {
211            format!("{}://{}", connection.scheme(), connection.host(),)
212        }
213    };
214    let time = util::get_system_time()?;
215    let mut expiry_date = header::parse_expiry_date(request.headers(), time)?;
216    if expiry_date.is_none() {
217        expiry_date = config
218            .read()
219            .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
220            .paste
221            .default_expiry
222            .and_then(|v| time.checked_add(v).map(|t| t.as_millis()));
223    }
224    let mut urls: Vec<String> = Vec::new();
225    while let Some(item) = payload.next().await {
226        let header_filename = header::parse_header_filename(request.headers())?;
227        let mut field = item?;
228        let content = ContentDisposition::from(
229            field
230                .content_disposition()
231                .ok_or_else(|| {
232                    error::ErrorInternalServerError("payload must contain content disposition")
233                })?
234                .clone(),
235        );
236        if let Ok(paste_type) = PasteType::try_from(&content) {
237            let mut bytes = Vec::<u8>::new();
238            while let Some(chunk) = field.next().await {
239                bytes.append(&mut chunk?.to_vec());
240            }
241            if bytes.is_empty() {
242                warn!("{} sent zero bytes", host);
243                return Err(error::ErrorBadRequest("invalid file size"));
244            }
245            if paste_type != PasteType::Oneshot
246                && paste_type != PasteType::RemoteFile
247                && paste_type != PasteType::OneshotUrl
248                && expiry_date.is_none()
249                && !config
250                    .read()
251                    .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
252                    .paste
253                    .duplicate_files
254                    .unwrap_or(true)
255            {
256                let bytes_checksum = util::sha256_digest(&*bytes)?;
257                let config = config
258                    .read()
259                    .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
260                if let Some(file) = Directory::try_from(config.server.upload_path.as_path())?
261                    .get_file(bytes_checksum)
262                {
263                    urls.push(format!(
264                        "{}/{}\n",
265                        server_url,
266                        file.path
267                            .file_name()
268                            .map(|v| v.to_string_lossy())
269                            .unwrap_or_default()
270                    ));
271                    continue;
272                }
273            }
274            let mut paste = Paste {
275                data: bytes.to_vec(),
276                type_: paste_type,
277            };
278            let mut file_name = match paste.type_ {
279                PasteType::File | PasteType::Oneshot => {
280                    let config = config
281                        .read()
282                        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
283                    paste.store_file(
284                        content.get_file_name()?,
285                        expiry_date,
286                        header_filename,
287                        &config,
288                    )?
289                }
290                PasteType::RemoteFile => {
291                    paste
292                        .store_remote_file(expiry_date, &client, &config)
293                        .await?
294                }
295                PasteType::Url | PasteType::OneshotUrl => {
296                    let config = config
297                        .read()
298                        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
299                    paste.store_url(expiry_date, header_filename, &config)?
300                }
301            };
302            info!(
303                "{} ({}) is uploaded from {}",
304                file_name,
305                Byte::from_u128(paste.data.len() as u128)
306                    .unwrap_or_default()
307                    .get_appropriate_unit(UnitType::Decimal),
308                host
309            );
310            let config = config
311                .read()
312                .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
313            if let Some(handle_spaces_config) = config.server.handle_spaces {
314                file_name = handle_spaces_config.process_filename(&file_name);
315            }
316            urls.push(format!("{}/{}\n", server_url, file_name));
317        } else {
318            warn!("{} sent an invalid form field", host);
319            return Err(error::ErrorBadRequest("invalid form field"));
320        }
321    }
322    Ok(HttpResponse::Ok().body(urls.join("")))
323}
324
325/// File entry item for list endpoint.
326#[derive(Serialize, Deserialize)]
327pub struct ListItem {
328    /// Uploaded file name.
329    pub file_name: PathBuf,
330    /// Size of the file in bytes.
331    pub file_size: u64,
332    /// ISO8601 formatted date-time of the moment the file was created (uploaded).
333    pub creation_date_utc: Option<String>,
334    /// ISO8601 formatted date-time string of the expiration timestamp if one exists for this file.
335    pub expires_at_utc: Option<String>,
336}
337
338/// Returns the list of files.
339#[get("/list")]
340#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
341async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
342    let config = config
343        .read()
344        .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
345        .clone();
346    if !config.server.expose_list.unwrap_or(false) {
347        warn!("server is not configured to expose list endpoint");
348        Err(error::ErrorNotFound(""))?;
349    }
350    let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
351        .filter_map(|entry| {
352            entry.ok().and_then(|e| {
353                let metadata = match e.metadata() {
354                    Ok(metadata) => {
355                        if metadata.is_dir() {
356                            return None;
357                        }
358                        metadata
359                    }
360                    Err(e) => {
361                        error!("failed to read metadata: {e}");
362                        return None;
363                    }
364                };
365                let mut file_name = PathBuf::from(e.file_name());
366
367                let creation_date_utc = metadata.created().ok().map(|v| {
368                    let millis = v
369                        .duration_since(UNIX_EPOCH)
370                        .expect("Time since UNIX epoch should be valid.")
371                        .as_millis();
372                    uts2ts::uts2ts(
373                        i64::try_from(millis).expect("UNIX time should be smaller than i64::MAX")
374                            / 1000,
375                    )
376                    .as_string()
377                });
378
379                let expires_at_utc = if let Some(expiration) = file_name
380                    .extension()
381                    .and_then(|ext| ext.to_str())
382                    .and_then(|v| v.parse::<i64>().ok())
383                {
384                    file_name.set_extension("");
385                    if util::get_system_time().ok()?
386                        > Duration::from_millis(expiration.try_into().ok()?)
387                    {
388                        return None;
389                    }
390                    Some(uts2ts::uts2ts(expiration / 1000).as_string())
391                } else {
392                    None
393                };
394                Some(ListItem {
395                    file_name,
396                    file_size: metadata.len(),
397                    creation_date_utc,
398                    expires_at_utc,
399                })
400            })
401        })
402        .collect();
403    Ok(HttpResponse::Ok().json(entries))
404}
405
406/// Configures the server routes.
407pub fn configure_routes(cfg: &mut web::ServiceConfig) {
408    cfg.service(
409        web::scope("")
410            .service(index)
411            .service(version)
412            .service(list)
413            .service(serve)
414            .service(upload)
415            .service(delete)
416            .route("", web::head().to(HttpResponse::MethodNotAllowed))
417            .wrap(GrantsMiddleware::with_extractor(extract_tokens))
418            .wrap(
419                ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized_error),
420            ),
421    );
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427    use crate::config::LandingPageConfig;
428    use crate::middleware::ContentLengthLimiter;
429    use crate::random::{RandomURLConfig, RandomURLType};
430    use actix_web::body::MessageBody;
431    use actix_web::body::{BodySize, BoxBody};
432    use actix_web::error::Error;
433    use actix_web::http::header::AUTHORIZATION;
434    use actix_web::http::{header, StatusCode};
435    use actix_web::test::{self, TestRequest};
436    use actix_web::web::Data;
437    use actix_web::App;
438    use awc::ClientBuilder;
439    use glob::glob;
440    use std::fs::File;
441    use std::io::Write;
442    use std::path::PathBuf;
443    use std::str;
444    use std::thread;
445    use std::time::Duration;
446
447    fn get_multipart_request(data: &str, name: &str, filename: &str) -> TestRequest {
448        let multipart_data = format!(
449            "\r\n\
450             --multipart_bound\r\n\
451             Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n\
452             Content-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\n\r\n\
453             {}\r\n\
454             --multipart_bound--\r\n",
455            name,
456            filename,
457            data.bytes().len(),
458            data,
459        );
460        TestRequest::post()
461            .insert_header((
462                header::CONTENT_TYPE,
463                header::HeaderValue::from_static("multipart/mixed; boundary=\"multipart_bound\""),
464            ))
465            .insert_header((
466                header::CONTENT_LENGTH,
467                header::HeaderValue::from_str(&data.bytes().len().to_string())
468                    .expect("cannot create header value"),
469            ))
470            .set_payload(multipart_data)
471    }
472
473    async fn assert_body(body: BoxBody, expected: &str) -> Result<(), Error> {
474        if let BodySize::Sized(size) = body.size() {
475            assert_eq!(size, expected.len() as u64);
476            let body_bytes = actix_web::body::to_bytes(body).await?;
477            let body_text = str::from_utf8(&body_bytes)?;
478            assert_eq!(expected, body_text);
479            Ok(())
480        } else {
481            Err(error::ErrorInternalServerError("unexpected body type"))
482        }
483    }
484
485    #[actix_web::test]
486    async fn test_index() {
487        let config = Config::default();
488        let app = test::init_service(
489            App::new()
490                .app_data(Data::new(RwLock::new(config)))
491                .service(index),
492        )
493        .await;
494        let request = TestRequest::default()
495            .insert_header(("content-type", "text/plain"))
496            .to_request();
497        let response = test::call_service(&app, request).await;
498        assert_eq!(StatusCode::FOUND, response.status());
499    }
500
501    #[actix_web::test]
502    async fn test_index_with_landing_page() -> Result<(), Error> {
503        let config = Config {
504            landing_page: Some(LandingPageConfig {
505                text: Some(String::from("landing page")),
506                ..Default::default()
507            }),
508            ..Default::default()
509        };
510        let app = test::init_service(
511            App::new()
512                .app_data(Data::new(RwLock::new(config)))
513                .service(index),
514        )
515        .await;
516        let request = TestRequest::default()
517            .insert_header(("content-type", "text/plain"))
518            .to_request();
519        let response = test::call_service(&app, request).await;
520        assert_eq!(StatusCode::OK, response.status());
521        assert_body(response.into_body(), "landing page").await?;
522        Ok(())
523    }
524
525    #[actix_web::test]
526    async fn test_index_with_landing_page_file() -> Result<(), Error> {
527        let filename = "landing_page.txt";
528        let config = Config {
529            landing_page: Some(LandingPageConfig {
530                file: Some(filename.to_string()),
531                ..Default::default()
532            }),
533            ..Default::default()
534        };
535        let mut file = File::create(filename)?;
536        file.write_all("landing page from file".as_bytes())?;
537        let app = test::init_service(
538            App::new()
539                .app_data(Data::new(RwLock::new(config)))
540                .service(index),
541        )
542        .await;
543        let request = TestRequest::default()
544            .insert_header(("content-type", "text/plain"))
545            .to_request();
546        let response = test::call_service(&app, request).await;
547        assert_eq!(StatusCode::OK, response.status());
548        assert_body(response.into_body(), "landing page from file").await?;
549        fs::remove_file(filename)?;
550        Ok(())
551    }
552
553    #[actix_web::test]
554    async fn test_index_with_landing_page_file_not_found() -> Result<(), Error> {
555        let filename = "landing_page.txt";
556        let config = Config {
557            landing_page: Some(LandingPageConfig {
558                text: Some(String::from("landing page")),
559                file: Some(filename.to_string()),
560                ..Default::default()
561            }),
562            ..Default::default()
563        };
564        let app = test::init_service(
565            App::new()
566                .app_data(Data::new(RwLock::new(config)))
567                .service(index),
568        )
569        .await;
570        let request = TestRequest::default()
571            .insert_header(("content-type", "text/plain"))
572            .to_request();
573        let response = test::call_service(&app, request).await;
574        assert_eq!(StatusCode::FOUND, response.status());
575        Ok(())
576    }
577
578    #[actix_web::test]
579    async fn test_version_without_auth() -> Result<(), Error> {
580        let mut config = Config::default();
581        config.server.auth_tokens = Some(["test".to_string()].into());
582        let app = test::init_service(
583            App::new()
584                .app_data(Data::new(RwLock::new(config)))
585                .app_data(Data::new(Client::default()))
586                .configure(configure_routes),
587        )
588        .await;
589
590        let request = TestRequest::default()
591            .insert_header(("content-type", "text/plain"))
592            .uri("/version")
593            .to_request();
594        let response = test::call_service(&app, request).await;
595        assert_eq!(StatusCode::UNAUTHORIZED, response.status());
596        assert_body(response.into_body(), "unauthorized\n").await?;
597        Ok(())
598    }
599
600    #[actix_web::test]
601    async fn test_version_without_config() -> Result<(), Error> {
602        let app = test::init_service(
603            App::new()
604                .app_data(Data::new(RwLock::new(Config::default())))
605                .app_data(Data::new(Client::default()))
606                .configure(configure_routes),
607        )
608        .await;
609
610        let request = TestRequest::default()
611            .insert_header(("content-type", "text/plain"))
612            .uri("/version")
613            .to_request();
614        let response = test::call_service(&app, request).await;
615        assert_eq!(StatusCode::NOT_FOUND, response.status());
616        assert_body(response.into_body(), "").await?;
617        Ok(())
618    }
619
620    #[actix_web::test]
621    async fn test_version() -> Result<(), Error> {
622        let mut config = Config::default();
623        config.server.expose_version = Some(true);
624        let app = test::init_service(
625            App::new()
626                .app_data(Data::new(RwLock::new(config)))
627                .app_data(Data::new(Client::default()))
628                .configure(configure_routes),
629        )
630        .await;
631
632        let request = TestRequest::default()
633            .insert_header(("content-type", "text/plain"))
634            .uri("/version")
635            .to_request();
636        let response = test::call_service(&app, request).await;
637        assert_eq!(StatusCode::OK, response.status());
638        assert_body(
639            response.into_body(),
640            &(env!("CARGO_PKG_VERSION").to_owned() + "\n"),
641        )
642        .await?;
643        Ok(())
644    }
645
646    #[actix_web::test]
647    async fn test_list() -> Result<(), Error> {
648        let mut config = Config::default();
649        config.server.expose_list = Some(true);
650
651        let test_upload_dir = "test_upload";
652        fs::create_dir(test_upload_dir)?;
653        config.server.upload_path = PathBuf::from(test_upload_dir);
654
655        let app = test::init_service(
656            App::new()
657                .app_data(Data::new(RwLock::new(config)))
658                .app_data(Data::new(Client::default()))
659                .configure(configure_routes),
660        )
661        .await;
662
663        let filename = "test_file.txt";
664        let timestamp = util::get_system_time()?.as_secs().to_string();
665        test::call_service(
666            &app,
667            get_multipart_request(&timestamp, "file", filename).to_request(),
668        )
669        .await;
670
671        let request = TestRequest::default()
672            .insert_header(("content-type", "text/plain"))
673            .uri("/list")
674            .to_request();
675        let result: Vec<ListItem> = test::call_and_read_body_json(&app, request).await;
676
677        assert_eq!(result.len(), 1);
678        assert_eq!(
679            result.first().expect("json object").file_name,
680            PathBuf::from(filename)
681        );
682
683        fs::remove_dir_all(test_upload_dir)?;
684
685        Ok(())
686    }
687
688    #[actix_web::test]
689    async fn test_list_expired() -> Result<(), Error> {
690        let mut config = Config::default();
691        config.server.expose_list = Some(true);
692
693        let test_upload_dir = "test_upload";
694        fs::create_dir(test_upload_dir)?;
695        config.server.upload_path = PathBuf::from(test_upload_dir);
696
697        let app = test::init_service(
698            App::new()
699                .app_data(Data::new(RwLock::new(config)))
700                .app_data(Data::new(Client::default()))
701                .configure(configure_routes),
702        )
703        .await;
704
705        let filename = "test_file.txt";
706        let timestamp = util::get_system_time()?.as_secs().to_string();
707        test::call_service(
708            &app,
709            get_multipart_request(&timestamp, "file", filename)
710                .insert_header((
711                    header::HeaderName::from_static("expire"),
712                    header::HeaderValue::from_static("50ms"),
713                ))
714                .to_request(),
715        )
716        .await;
717
718        thread::sleep(Duration::from_millis(500));
719
720        let request = TestRequest::default()
721            .insert_header(("content-type", "text/plain"))
722            .uri("/list")
723            .to_request();
724        let result: Vec<ListItem> = test::call_and_read_body_json(&app, request).await;
725
726        assert!(result.is_empty());
727
728        fs::remove_dir_all(test_upload_dir)?;
729
730        Ok(())
731    }
732
733    #[actix_web::test]
734    async fn test_auth() -> Result<(), Error> {
735        let mut config = Config::default();
736        config.server.auth_tokens = Some(["test".to_string()].into());
737
738        let app = test::init_service(
739            App::new()
740                .app_data(Data::new(RwLock::new(config)))
741                .app_data(Data::new(Client::default()))
742                .configure(configure_routes),
743        )
744        .await;
745
746        let response =
747            test::call_service(&app, get_multipart_request("", "", "").to_request()).await;
748        assert_eq!(StatusCode::UNAUTHORIZED, response.status());
749        assert_body(response.into_body(), "unauthorized\n").await?;
750
751        Ok(())
752    }
753
754    #[actix_web::test]
755    async fn test_payload_limit() -> Result<(), Error> {
756        let app = test::init_service(
757            App::new()
758                .app_data(Data::new(RwLock::new(Config::default())))
759                .app_data(Data::new(Client::default()))
760                .wrap(ContentLengthLimiter::new(Byte::from_u64(1)))
761                .configure(configure_routes),
762        )
763        .await;
764
765        let response = test::call_service(
766            &app,
767            get_multipart_request("test", "file", "test").to_request(),
768        )
769        .await;
770        assert_eq!(StatusCode::PAYLOAD_TOO_LARGE, response.status());
771        assert_body(response.into_body().boxed(), "upload limit exceeded").await?;
772
773        Ok(())
774    }
775
776    #[actix_web::test]
777    async fn test_delete_file() -> Result<(), Error> {
778        let mut config = Config::default();
779        config.server.delete_tokens = Some(["test".to_string()].into());
780        config.server.upload_path = env::current_dir()?;
781
782        let app = test::init_service(
783            App::new()
784                .app_data(Data::new(RwLock::new(config)))
785                .app_data(Data::new(Client::default()))
786                .configure(configure_routes),
787        )
788        .await;
789
790        let file_name = "test_file.txt";
791        let timestamp = util::get_system_time()?.as_secs().to_string();
792        test::call_service(
793            &app,
794            get_multipart_request(&timestamp, "file", file_name).to_request(),
795        )
796        .await;
797
798        let request = TestRequest::delete()
799            .insert_header((AUTHORIZATION, header::HeaderValue::from_static("test")))
800            .uri(&format!("/{file_name}"))
801            .to_request();
802        let response = test::call_service(&app, request).await;
803
804        assert_eq!(StatusCode::OK, response.status());
805        assert_body(response.into_body(), "file deleted\n").await?;
806
807        let path = PathBuf::from(file_name);
808        assert!(!path.exists());
809
810        Ok(())
811    }
812
813    #[actix_web::test]
814    async fn test_delete_file_without_token_in_config() -> Result<(), Error> {
815        let mut config = Config::default();
816        config.server.upload_path = env::current_dir()?;
817
818        let app = test::init_service(
819            App::new()
820                .app_data(Data::new(RwLock::new(config)))
821                .app_data(Data::new(Client::default()))
822                .configure(configure_routes),
823        )
824        .await;
825
826        let file_name = "test_file.txt";
827        let request = TestRequest::delete()
828            .insert_header((AUTHORIZATION, header::HeaderValue::from_static("test")))
829            .uri(&format!("/{file_name}"))
830            .to_request();
831        let response = test::call_service(&app, request).await;
832
833        assert_eq!(StatusCode::NOT_FOUND, response.status());
834        assert_body(response.into_body(), "").await?;
835
836        Ok(())
837    }
838
839    #[actix_web::test]
840    async fn test_upload_file() -> Result<(), Error> {
841        let mut config = Config::default();
842        config.server.upload_path = env::current_dir()?;
843
844        let app = test::init_service(
845            App::new()
846                .app_data(Data::new(RwLock::new(config)))
847                .app_data(Data::new(Client::default()))
848                .configure(configure_routes),
849        )
850        .await;
851
852        let file_name = "test_file.txt";
853        let timestamp = util::get_system_time()?.as_secs().to_string();
854        let response = test::call_service(
855            &app,
856            get_multipart_request(&timestamp, "file", file_name).to_request(),
857        )
858        .await;
859        assert_eq!(StatusCode::OK, response.status());
860        assert_body(
861            response.into_body(),
862            &format!("http://localhost:8080/{file_name}\n"),
863        )
864        .await?;
865
866        let serve_request = TestRequest::get()
867            .uri(&format!("/{file_name}"))
868            .to_request();
869        let response = test::call_service(&app, serve_request).await;
870        assert_eq!(StatusCode::OK, response.status());
871        assert_body(response.into_body(), &timestamp).await?;
872
873        fs::remove_file(file_name)?;
874        let serve_request = TestRequest::get()
875            .uri(&format!("/{file_name}"))
876            .to_request();
877        let response = test::call_service(&app, serve_request).await;
878        assert_eq!(StatusCode::NOT_FOUND, response.status());
879
880        Ok(())
881    }
882
883    #[actix_web::test]
884    async fn test_upload_file_override_filename() -> Result<(), Error> {
885        let mut config = Config::default();
886        config.server.upload_path = env::current_dir()?;
887
888        let app = test::init_service(
889            App::new()
890                .app_data(Data::new(RwLock::new(config)))
891                .app_data(Data::new(Client::default()))
892                .configure(configure_routes),
893        )
894        .await;
895
896        let file_name = "test_file.txt";
897        let header_filename = "fn_from_header.txt";
898        let timestamp = util::get_system_time()?.as_secs().to_string();
899        let response = test::call_service(
900            &app,
901            get_multipart_request(&timestamp, "file", file_name)
902                .insert_header((
903                    header::HeaderName::from_static("filename"),
904                    header::HeaderValue::from_static("fn_from_header.txt"),
905                ))
906                .to_request(),
907        )
908        .await;
909        assert_eq!(StatusCode::OK, response.status());
910        assert_body(
911            response.into_body(),
912            &format!("http://localhost:8080/{header_filename}\n"),
913        )
914        .await?;
915
916        let serve_request = TestRequest::get()
917            .uri(&format!("/{header_filename}"))
918            .to_request();
919        let response = test::call_service(&app, serve_request).await;
920        assert_eq!(StatusCode::OK, response.status());
921        assert_body(response.into_body(), &timestamp).await?;
922
923        fs::remove_file(header_filename)?;
924        let serve_request = TestRequest::get()
925            .uri(&format!("/{header_filename}"))
926            .to_request();
927        let response = test::call_service(&app, serve_request).await;
928        assert_eq!(StatusCode::NOT_FOUND, response.status());
929
930        Ok(())
931    }
932
933    #[actix_web::test]
934    async fn test_upload_same_filename() -> Result<(), Error> {
935        let mut config = Config::default();
936        config.server.upload_path = env::current_dir()?;
937
938        let app = test::init_service(
939            App::new()
940                .app_data(Data::new(RwLock::new(config)))
941                .app_data(Data::new(Client::default()))
942                .configure(configure_routes),
943        )
944        .await;
945
946        let file_name = "test_file.txt";
947        let header_filename = "fn_from_header.txt";
948        let timestamp = util::get_system_time()?.as_secs().to_string();
949        let response = test::call_service(
950            &app,
951            get_multipart_request(&timestamp, "file", file_name)
952                .insert_header((
953                    header::HeaderName::from_static("filename"),
954                    header::HeaderValue::from_static("fn_from_header.txt"),
955                ))
956                .to_request(),
957        )
958        .await;
959        assert_eq!(StatusCode::OK, response.status());
960        assert_body(
961            response.into_body(),
962            &format!("http://localhost:8080/{header_filename}\n"),
963        )
964        .await?;
965
966        let timestamp = util::get_system_time()?.as_secs().to_string();
967        let response = test::call_service(
968            &app,
969            get_multipart_request(&timestamp, "file", file_name)
970                .insert_header((
971                    header::HeaderName::from_static("filename"),
972                    header::HeaderValue::from_static("fn_from_header.txt"),
973                ))
974                .to_request(),
975        )
976        .await;
977        assert_eq!(StatusCode::CONFLICT, response.status());
978        assert_body(response.into_body(), "file already exists\n").await?;
979
980        fs::remove_file(header_filename)?;
981
982        Ok(())
983    }
984
985    #[actix_web::test]
986    #[allow(deprecated)]
987    async fn test_upload_duplicate_file() -> Result<(), Error> {
988        let test_upload_dir = "test_upload";
989        fs::create_dir(test_upload_dir)?;
990
991        let mut config = Config::default();
992        config.server.upload_path = PathBuf::from(&test_upload_dir);
993        config.paste.duplicate_files = Some(false);
994        config.paste.random_url = Some(RandomURLConfig {
995            enabled: Some(true),
996            type_: RandomURLType::Alphanumeric,
997            ..Default::default()
998        });
999
1000        let app = test::init_service(
1001            App::new()
1002                .app_data(Data::new(RwLock::new(config)))
1003                .app_data(Data::new(Client::default()))
1004                .configure(configure_routes),
1005        )
1006        .await;
1007
1008        let response = test::call_service(
1009            &app,
1010            get_multipart_request("test", "file", "x").to_request(),
1011        )
1012        .await;
1013        assert_eq!(StatusCode::OK, response.status());
1014        let body = response.into_body();
1015        let first_body_bytes = actix_web::body::to_bytes(body).await?;
1016
1017        let response = test::call_service(
1018            &app,
1019            get_multipart_request("test", "file", "x").to_request(),
1020        )
1021        .await;
1022        assert_eq!(StatusCode::OK, response.status());
1023        let body = response.into_body();
1024        let second_body_bytes = actix_web::body::to_bytes(body).await?;
1025
1026        assert_eq!(first_body_bytes, second_body_bytes);
1027
1028        fs::remove_dir_all(test_upload_dir)?;
1029
1030        Ok(())
1031    }
1032
1033    #[actix_web::test]
1034    async fn test_upload_expiring_file() -> Result<(), Error> {
1035        let mut config = Config::default();
1036        config.server.upload_path = env::current_dir()?;
1037
1038        let app = test::init_service(
1039            App::new()
1040                .app_data(Data::new(RwLock::new(config)))
1041                .app_data(Data::new(Client::default()))
1042                .configure(configure_routes),
1043        )
1044        .await;
1045
1046        let file_name = "test_file.txt";
1047        let timestamp = util::get_system_time()?.as_secs().to_string();
1048        let response = test::call_service(
1049            &app,
1050            get_multipart_request(&timestamp, "file", file_name)
1051                .insert_header((
1052                    header::HeaderName::from_static("expire"),
1053                    header::HeaderValue::from_static("20ms"),
1054                ))
1055                .to_request(),
1056        )
1057        .await;
1058        assert_eq!(StatusCode::OK, response.status());
1059        assert_body(
1060            response.into_body(),
1061            &format!("http://localhost:8080/{file_name}\n"),
1062        )
1063        .await?;
1064
1065        let serve_request = TestRequest::get()
1066            .uri(&format!("/{file_name}"))
1067            .to_request();
1068        let response = test::call_service(&app, serve_request).await;
1069        assert_eq!(StatusCode::OK, response.status());
1070        assert_body(response.into_body(), &timestamp).await?;
1071
1072        thread::sleep(Duration::from_millis(40));
1073
1074        let serve_request = TestRequest::get()
1075            .uri(&format!("/{file_name}"))
1076            .to_request();
1077        let response = test::call_service(&app, serve_request).await;
1078        assert_eq!(StatusCode::NOT_FOUND, response.status());
1079
1080        if let Some(glob_path) = glob(&format!("{file_name}.[0-9]*"))
1081            .map_err(error::ErrorInternalServerError)?
1082            .next()
1083        {
1084            fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?;
1085        }
1086
1087        Ok(())
1088    }
1089
1090    #[actix_web::test]
1091    async fn test_upload_remote_file() -> Result<(), Error> {
1092        let mut config = Config::default();
1093        config.server.upload_path = env::current_dir()?;
1094        config.server.max_content_length = Byte::from_u128(30000).unwrap_or_default();
1095
1096        let app = test::init_service(
1097            App::new()
1098                .app_data(Data::new(RwLock::new(config)))
1099                .app_data(Data::new(
1100                    ClientBuilder::new()
1101                        .timeout(Duration::from_secs(30))
1102                        .finish(),
1103                ))
1104                .configure(configure_routes),
1105        )
1106        .await;
1107
1108        let file_name =
1109            "rp_test_3b5eeeee7a7326cd6141f54820e6356a0e9d1dd4021407cb1d5e9de9f034ed2f.png";
1110        let response = test::call_service(
1111            &app,
1112            get_multipart_request(
1113                "https://raw.githubusercontent.com/orhun/rustypaste/refs/heads/master/img/rp_test_3b5eeeee7a7326cd6141f54820e6356a0e9d1dd4021407cb1d5e9de9f034ed2f.png",
1114                "remote",
1115                file_name,
1116            )
1117            .to_request(),
1118        )
1119        .await;
1120        assert_eq!(StatusCode::OK, response.status());
1121        assert_body(
1122            response.into_body().boxed(),
1123            &format!("http://localhost:8080/{file_name}\n"),
1124        )
1125        .await?;
1126
1127        let serve_request = TestRequest::get()
1128            .uri(&format!("/{file_name}"))
1129            .to_request();
1130        let response = test::call_service(&app, serve_request).await;
1131        assert_eq!(StatusCode::OK, response.status());
1132
1133        let body = response.into_body();
1134        let body_bytes = actix_web::body::to_bytes(body).await?;
1135        assert_eq!(
1136            "3b5eeeee7a7326cd6141f54820e6356a0e9d1dd4021407cb1d5e9de9f034ed2f",
1137            util::sha256_digest(&*body_bytes)?
1138        );
1139
1140        fs::remove_file(file_name)?;
1141
1142        let serve_request = TestRequest::get()
1143            .uri(&format!("/{file_name}"))
1144            .to_request();
1145        let response = test::call_service(&app, serve_request).await;
1146        assert_eq!(StatusCode::NOT_FOUND, response.status());
1147
1148        Ok(())
1149    }
1150
1151    #[actix_web::test]
1152    async fn test_upload_url() -> Result<(), Error> {
1153        let mut config = Config::default();
1154        config.server.upload_path = env::current_dir()?;
1155
1156        let app = test::init_service(
1157            App::new()
1158                .app_data(Data::new(RwLock::new(config.clone())))
1159                .app_data(Data::new(Client::default()))
1160                .configure(configure_routes),
1161        )
1162        .await;
1163
1164        let url_upload_path = PasteType::Url
1165            .get_path(&config.server.upload_path)
1166            .expect("Bad upload path");
1167        fs::create_dir_all(&url_upload_path)?;
1168
1169        let response = test::call_service(
1170            &app,
1171            get_multipart_request(env!("CARGO_PKG_HOMEPAGE"), "url", "").to_request(),
1172        )
1173        .await;
1174        assert_eq!(StatusCode::OK, response.status());
1175        assert_body(response.into_body(), "http://localhost:8080/url\n").await?;
1176
1177        let serve_request = TestRequest::get().uri("/url").to_request();
1178        let response = test::call_service(&app, serve_request).await;
1179        assert_eq!(StatusCode::FOUND, response.status());
1180
1181        fs::remove_file(url_upload_path.join("url"))?;
1182        fs::remove_dir(url_upload_path)?;
1183
1184        let serve_request = TestRequest::get().uri("/url").to_request();
1185        let response = test::call_service(&app, serve_request).await;
1186        assert_eq!(StatusCode::NOT_FOUND, response.status());
1187
1188        Ok(())
1189    }
1190
1191    #[actix_web::test]
1192    async fn test_upload_oneshot() -> Result<(), Error> {
1193        let mut config = Config::default();
1194        config.server.upload_path = env::current_dir()?;
1195
1196        let app = test::init_service(
1197            App::new()
1198                .app_data(Data::new(RwLock::new(config.clone())))
1199                .app_data(Data::new(Client::default()))
1200                .configure(configure_routes),
1201        )
1202        .await;
1203
1204        let oneshot_upload_path = PasteType::Oneshot
1205            .get_path(&config.server.upload_path)
1206            .expect("Bad upload path");
1207        fs::create_dir_all(&oneshot_upload_path)?;
1208
1209        let file_name = "oneshot.txt";
1210        let timestamp = util::get_system_time()?.as_secs().to_string();
1211        let response = test::call_service(
1212            &app,
1213            get_multipart_request(&timestamp, "oneshot", file_name).to_request(),
1214        )
1215        .await;
1216        assert_eq!(StatusCode::OK, response.status());
1217        assert_body(
1218            response.into_body(),
1219            &format!("http://localhost:8080/{file_name}\n"),
1220        )
1221        .await?;
1222
1223        let serve_request = TestRequest::get()
1224            .uri(&format!("/{file_name}"))
1225            .to_request();
1226        let response = test::call_service(&app, serve_request).await;
1227        assert_eq!(StatusCode::OK, response.status());
1228        assert_body(response.into_body(), &timestamp).await?;
1229
1230        let serve_request = TestRequest::get()
1231            .uri(&format!("/{file_name}"))
1232            .to_request();
1233        let response = test::call_service(&app, serve_request).await;
1234        assert_eq!(StatusCode::NOT_FOUND, response.status());
1235
1236        if let Some(glob_path) = glob(
1237            &oneshot_upload_path
1238                .join(format!("{file_name}.[0-9]*"))
1239                .to_string_lossy(),
1240        )
1241        .map_err(error::ErrorInternalServerError)?
1242        .next()
1243        {
1244            fs::remove_file(glob_path.map_err(error::ErrorInternalServerError)?)?;
1245        }
1246        fs::remove_dir(oneshot_upload_path)?;
1247
1248        Ok(())
1249    }
1250
1251    #[actix_web::test]
1252    async fn test_upload_oneshot_url() -> Result<(), Error> {
1253        let mut config = Config::default();
1254        config.server.upload_path = env::current_dir()?;
1255
1256        let oneshot_url_suffix = "oneshot_url";
1257
1258        let app = test::init_service(
1259            App::new()
1260                .app_data(Data::new(RwLock::new(config.clone())))
1261                .app_data(Data::new(Client::default()))
1262                .configure(configure_routes),
1263        )
1264        .await;
1265
1266        let url_upload_path = PasteType::OneshotUrl
1267            .get_path(&config.server.upload_path)
1268            .expect("Bad upload path");
1269        fs::create_dir_all(&url_upload_path)?;
1270
1271        let response = test::call_service(
1272            &app,
1273            get_multipart_request(
1274                env!("CARGO_PKG_HOMEPAGE"),
1275                oneshot_url_suffix,
1276                oneshot_url_suffix,
1277            )
1278            .to_request(),
1279        )
1280        .await;
1281        assert_eq!(StatusCode::OK, response.status());
1282        assert_body(
1283            response.into_body(),
1284            &format!("http://localhost:8080/{}\n", oneshot_url_suffix),
1285        )
1286        .await?;
1287
1288        // Make the oneshot_url request, ensure it is found.
1289        let serve_request = TestRequest::with_uri(&format!("/{}", oneshot_url_suffix)).to_request();
1290        let response = test::call_service(&app, serve_request).await;
1291        assert_eq!(StatusCode::FOUND, response.status());
1292
1293        // Make the same request again, and ensure that the oneshot_url is not found.
1294        let serve_request = TestRequest::with_uri(&format!("/{}", oneshot_url_suffix)).to_request();
1295        let response = test::call_service(&app, serve_request).await;
1296        assert_eq!(StatusCode::NOT_FOUND, response.status());
1297
1298        // Cleanup
1299        fs::remove_dir_all(url_upload_path)?;
1300
1301        Ok(())
1302    }
1303}