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:
Leonora Tindall
2022-02-04 13:47:30 -06:00
committed by Gunwant Jain
parent 55ed495b83
commit 2ab7ddb9c8
12 changed files with 292 additions and 153 deletions

View File

@@ -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)

View File

@@ -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),
}
}
}

View File

@@ -1,4 +1,4 @@
pub mod maybe_redirect;
pub mod paste_id;
pub mod pretty;
pub mod pretty_syntax;
pub mod response_wrapper;

View 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())
}

View File

@@ -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;

View File

@@ -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"),
}
}

View File

@@ -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,
}
}

View File

@@ -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)
}

View File

@@ -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))
}