Add cache-busting and server headers with a wrapper.
In order to support custom headers for various response types, this commit adds a wrapper type, ResponseWrapper, which can service all types of response in `bin`. For paste objects, the preferred `Last-Modified` is used, so that caches can compare their exact timings with the HEAD response when revalidating. For static objects, an `ETag` is used instead, based on the Cargo version and git hash of the codebase at compilation time; a `build.rs` is used for this.
This commit is contained in:
committed by
Gunwant Jain
parent
55ed495b83
commit
2ab7ddb9c8
15
src/main.rs
15
src/main.rs
@@ -3,6 +3,7 @@ extern crate rocket;
|
||||
use std::{fs, net::IpAddr, path::PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use once_cell::sync::Lazy;
|
||||
use rocket::{
|
||||
figment::{providers::Env, Figment},
|
||||
shield::{NoSniff, Shield},
|
||||
@@ -13,6 +14,18 @@ use rust_embed::RustEmbed;
|
||||
mod models;
|
||||
mod routes;
|
||||
|
||||
const BINARY_VERSION: &str =
|
||||
concat!(env!("CARGO_PKG_VERSION"), env!("GIT_HASH"));
|
||||
const SERVER_VERSION: &str = concat!(
|
||||
"bin v.",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (",
|
||||
env!("GIT_HASH"),
|
||||
") (Rocket)"
|
||||
);
|
||||
static BINARY_ETAG: Lazy<String> =
|
||||
Lazy::new(|| sha256::digest(BINARY_VERSION));
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "templates/"]
|
||||
struct EmbeddedTemplates;
|
||||
@@ -91,7 +104,7 @@ fn rocket() -> _ {
|
||||
routes::retrieve::retrieve,
|
||||
routes::retrieve::retrieve_ext,
|
||||
routes::pretty_retrieve::pretty_retrieve,
|
||||
routes::pretty_retrieve_ext::pretty_retrieve_ext
|
||||
routes::pretty_retrieve::pretty_retrieve_ext
|
||||
],
|
||||
)
|
||||
.attach(shield)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
use rocket::{
|
||||
request::Request,
|
||||
response::{Redirect, Responder, Result},
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
pub enum MaybeRedirect {
|
||||
Redirect(Box<Redirect>),
|
||||
Template(Box<Template>),
|
||||
}
|
||||
|
||||
impl From<Redirect> for MaybeRedirect {
|
||||
fn from(other: Redirect) -> Self {
|
||||
Self::Redirect(Box::new(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Template> for MaybeRedirect {
|
||||
fn from(other: Template) -> Self {
|
||||
Self::Template(Box::new(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for MaybeRedirect {
|
||||
fn respond_to(self, req: &'r Request<'_>) -> Result<'o> {
|
||||
match self {
|
||||
Self::Template(t) => t.respond_to(req),
|
||||
Self::Redirect(r) => r.respond_to(req),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod maybe_redirect;
|
||||
pub mod paste_id;
|
||||
pub mod pretty;
|
||||
pub mod pretty_syntax;
|
||||
pub mod response_wrapper;
|
||||
|
||||
84
src/models/response_wrapper.rs
Normal file
84
src/models/response_wrapper.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::Request,
|
||||
response::{Redirect, Responder, Response, Result},
|
||||
};
|
||||
use std::io::Cursor;
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub enum ResponseWrapper<R> {
|
||||
MetaInterfaceResponse(R),
|
||||
PasteContentResponse(R, SystemTime),
|
||||
Redirect(Box<Redirect>),
|
||||
NotFound(String),
|
||||
ServerError(String),
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r, R: Responder<'r, 'o>> ResponseWrapper<R> {
|
||||
pub fn meta_response(responder: R) -> Self {
|
||||
Self::MetaInterfaceResponse(responder)
|
||||
}
|
||||
|
||||
pub fn paste_response(responder: R, modified: SystemTime) -> Self {
|
||||
Self::PasteContentResponse(responder, modified)
|
||||
}
|
||||
|
||||
pub fn redirect(redirect: Redirect) -> Self {
|
||||
Self::Redirect(Box::new(redirect))
|
||||
}
|
||||
|
||||
pub fn not_found(id: &str) -> Self {
|
||||
Self::NotFound(id.to_string())
|
||||
}
|
||||
|
||||
pub fn server_error<S: Into<String>>(message: S) -> Self {
|
||||
Self::ServerError(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o>
|
||||
for ResponseWrapper<R>
|
||||
{
|
||||
fn respond_to(self, request: &'r Request<'_>) -> Result<'o> {
|
||||
use ResponseWrapper::*;
|
||||
|
||||
// Add global headers.
|
||||
let mut response = Response::build();
|
||||
response.raw_header("Server", crate::SERVER_VERSION);
|
||||
|
||||
// Handle individual request types.
|
||||
match self {
|
||||
MetaInterfaceResponse(sup) => response
|
||||
.join(sup.respond_to(request)?)
|
||||
.raw_header("ETag", &*crate::BINARY_ETAG)
|
||||
.ok(),
|
||||
PasteContentResponse(sup, modified) => response
|
||||
.join(sup.respond_to(request)?)
|
||||
.raw_header("Last-Modified", http_strftime(modified))
|
||||
.ok(),
|
||||
Redirect(sup) => response.join(sup.respond_to(request)?).ok(),
|
||||
NotFound(s) => {
|
||||
let body = format!("Unable to find entity '{}'", s);
|
||||
|
||||
response
|
||||
.sized_body(body.len(), Cursor::new(body))
|
||||
.status(Status::NotFound)
|
||||
.ok()
|
||||
}
|
||||
|
||||
ServerError(s) => {
|
||||
let body = format!("Server error: '{}'", s);
|
||||
response
|
||||
.sized_body(body.len(), Cursor::new(body))
|
||||
.status(Status::InternalServerError)
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn http_strftime<T: Into<time::OffsetDateTime>>(time: T) -> String {
|
||||
time.into()
|
||||
.format(&time::format_description::well_known::Rfc2822)
|
||||
.unwrap_or_else(|_| "datetime unknown".into())
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod index;
|
||||
pub mod pretty_retrieve;
|
||||
pub mod pretty_retrieve_ext;
|
||||
pub mod retrieve;
|
||||
pub mod static_files;
|
||||
pub mod submit;
|
||||
|
||||
@@ -1,26 +1,62 @@
|
||||
use rocket::response::Redirect;
|
||||
use rocket_dyn_templates::Template;
|
||||
use std::fs;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind::InvalidData;
|
||||
use std::io::ErrorKind::{InvalidData, NotFound};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::get_upload_dir;
|
||||
use crate::models::maybe_redirect::MaybeRedirect;
|
||||
use crate::models::paste_id::PasteId;
|
||||
use crate::models::pretty::get_pretty_body;
|
||||
use crate::models::pretty_syntax::PasteIdSyntax;
|
||||
use crate::models::response_wrapper::ResponseWrapper;
|
||||
|
||||
#[get("/p/<id>", rank = 2)]
|
||||
pub async fn pretty_retrieve(id: PasteId<'_>) -> Option<MaybeRedirect> {
|
||||
let filepath = Path::new(&get_upload_dir()).join(format!("{id}", id = id));
|
||||
pub async fn pretty_retrieve(id: PasteId<'_>) -> ResponseWrapper<Template> {
|
||||
pretter_retrieve_inner(&id.to_string(), "txt").await
|
||||
}
|
||||
|
||||
let contents = match get_pretty_body(&filepath, &String::from("txt")) {
|
||||
#[get("/p/<id_ext>", rank = 1)]
|
||||
pub async fn pretty_retrieve_ext(
|
||||
id_ext: PasteIdSyntax<'_>,
|
||||
) -> ResponseWrapper<Template> {
|
||||
let id = id_ext.get_fname();
|
||||
let ext = id_ext.get_ext();
|
||||
|
||||
pretter_retrieve_inner(id, ext).await
|
||||
}
|
||||
|
||||
pub async fn pretter_retrieve_inner(
|
||||
id: &str,
|
||||
ext: &str,
|
||||
) -> ResponseWrapper<Template> {
|
||||
let filepath = Path::new(&get_upload_dir()).join(id.to_string());
|
||||
|
||||
let modified_date =
|
||||
match fs::metadata(&filepath).and_then(|m| m.modified()) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
return ResponseWrapper::not_found(id);
|
||||
}
|
||||
Err(e) => {
|
||||
return ResponseWrapper::server_error(e.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let contents = match get_pretty_body(&filepath, ext) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == InvalidData => {
|
||||
return Some(Redirect::to(format!("/{}", id)).into());
|
||||
return ResponseWrapper::redirect(Redirect::permanent(format!(
|
||||
"/{}",
|
||||
id
|
||||
)));
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
return ResponseWrapper::not_found(id)
|
||||
}
|
||||
Err(e) => {
|
||||
return ResponseWrapper::server_error(e.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,7 +66,7 @@ pub async fn pretty_retrieve(id: PasteId<'_>) -> Option<MaybeRedirect> {
|
||||
let rendered = Template::render("pretty.html", &map);
|
||||
|
||||
match tree_magic::match_filepath("text/plain", &filepath) {
|
||||
true => Some(rendered.into()),
|
||||
false => None,
|
||||
true => ResponseWrapper::paste_response(rendered, modified_date),
|
||||
false => ResponseWrapper::server_error("media type unacceptable"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
use rocket::response::Redirect;
|
||||
use rocket_dyn_templates::Template;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind::InvalidData;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::get_upload_dir;
|
||||
use crate::models::maybe_redirect::MaybeRedirect;
|
||||
use crate::models::pretty::get_pretty_body;
|
||||
use crate::models::pretty_syntax::PasteIdSyntax;
|
||||
|
||||
#[get("/p/<id_ext>", rank = 1)]
|
||||
pub async fn pretty_retrieve_ext(
|
||||
id_ext: PasteIdSyntax<'_>,
|
||||
) -> Option<MaybeRedirect> {
|
||||
let id = id_ext.get_fname();
|
||||
let ext = id_ext.get_ext();
|
||||
|
||||
let filepath = Path::new(&get_upload_dir()).join(id.to_string());
|
||||
|
||||
let contents = match get_pretty_body(&filepath, &ext.to_string()) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == InvalidData => {
|
||||
return Some(Redirect::to(format!("/{}", id)).into());
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("title", id.to_string());
|
||||
map.insert("body", contents);
|
||||
let rendered = Template::render("pretty.html", &map);
|
||||
|
||||
match tree_magic::match_filepath("text/plain", &filepath) {
|
||||
true => Some(rendered.into()),
|
||||
false => None,
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,46 @@
|
||||
use std::fs::File;
|
||||
use std::io::ErrorKind::NotFound;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::get_upload_dir;
|
||||
use crate::models::paste_id::PasteId;
|
||||
use crate::models::pretty_syntax::PasteIdSyntax;
|
||||
use crate::models::response_wrapper::ResponseWrapper;
|
||||
|
||||
#[get("/<id>", rank = 2)]
|
||||
pub async fn retrieve(id: PasteId<'_>) -> Option<File> {
|
||||
// let filename = format!("upload/{id}", id = id);
|
||||
|
||||
File::open(get_upload_dir().join(format!("{id}", id = id))).ok()
|
||||
#[get("/r/<id>", rank = 2)]
|
||||
pub async fn retrieve(id: PasteId<'_>) -> ResponseWrapper<File> {
|
||||
retrieve_inner(&id.to_string()).await
|
||||
}
|
||||
|
||||
// rank 1 here because this would be more oftenly used
|
||||
#[get("/<id_ext>", rank = 1)]
|
||||
pub async fn retrieve_ext(id_ext: PasteIdSyntax<'_>) -> Option<File> {
|
||||
// let filename = format!("upload/{id}", id = id_ext.get_fname());
|
||||
|
||||
File::open(get_upload_dir().join(id_ext.get_fname().to_string())).ok()
|
||||
#[get("/r/<id_ext>", rank = 1)]
|
||||
pub async fn retrieve_ext(id_ext: PasteIdSyntax<'_>) -> ResponseWrapper<File> {
|
||||
retrieve_inner(&id_ext.get_fname().to_string()).await
|
||||
}
|
||||
|
||||
pub async fn retrieve_inner(id: &str) -> ResponseWrapper<File> {
|
||||
let filepath = Path::new(&get_upload_dir()).join(id.to_string());
|
||||
|
||||
let modified_date =
|
||||
match std::fs::metadata(&filepath).and_then(|m| m.modified()) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
return ResponseWrapper::not_found(id);
|
||||
}
|
||||
Err(e) => {
|
||||
return ResponseWrapper::server_error(e.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let file = match File::open(&filepath) {
|
||||
Ok(v) => v,
|
||||
Err(e) if e.kind() == NotFound => {
|
||||
return ResponseWrapper::not_found(id)
|
||||
}
|
||||
Err(e) => {
|
||||
return ResponseWrapper::server_error(e.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
ResponseWrapper::paste_response(file, modified_date)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::models::response_wrapper::ResponseWrapper;
|
||||
use rocket::http::ContentType;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, ffi::OsStr, path::PathBuf};
|
||||
@@ -9,9 +10,12 @@ struct Static;
|
||||
#[get("/static/<file..>")]
|
||||
pub fn static_files(
|
||||
file: PathBuf,
|
||||
) -> Option<(ContentType, Cow<'static, [u8]>)> {
|
||||
) -> ResponseWrapper<(ContentType, Cow<'static, [u8]>)> {
|
||||
let filename = file.display().to_string();
|
||||
let asset = Static::get(&filename)?;
|
||||
let asset = match Static::get(&filename) {
|
||||
Some(v) => v,
|
||||
None => return ResponseWrapper::not_found(&file.to_string_lossy()),
|
||||
};
|
||||
|
||||
let content_type = file
|
||||
.extension()
|
||||
@@ -19,5 +23,5 @@ pub fn static_files(
|
||||
.and_then(ContentType::from_extension)
|
||||
.unwrap_or(ContentType::Bytes);
|
||||
|
||||
Some((content_type, asset.data))
|
||||
ResponseWrapper::meta_response((content_type, asset.data))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user