Skip to content

Commit

Permalink
Add maximum expiration (#62)
Browse files Browse the repository at this point in the history
* Add maximum expiry

* apply review

expiry -> expiration
hard fail on invalid expiration values

---------

Co-authored-by: HeapUnderflow <[email protected]>
  • Loading branch information
HeapUnderfl0w and HeapUnderflow authored Jul 10, 2024
1 parent d361b66 commit 000d30f
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ run-time behavior:
until wastebin responds with 408, by default it is set to 5 seconds.
* `WASTEBIN_MAX_BODY_SIZE` number of bytes to accept for POST requests. Defaults
to 1 MB.
* `WASTEBIN_MAX_PASTE_EXPIRATION` maximum allowed lifetime of a paste in seconds. Leave empty or set to -1 for no limit. Defaults to no limit.
* `WASTEBIN_PASSWORD_SALT` salt used to hash user passwords used for encrypting
pastes.
* `WASTEBIN_SIGNING_KEY` sets the key to sign cookies. If not set, a random key
Expand Down
22 changes: 22 additions & 0 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const VAR_SIGNING_KEY: &str = "WASTEBIN_SIGNING_KEY";
const VAR_BASE_URL: &str = "WASTEBIN_BASE_URL";
const VAR_PASSWORD_SALT: &str = "WASTEBIN_PASSWORD_SALT";
const VAR_HTTP_TIMEOUT: &str = "WASTEBIN_HTTP_TIMEOUT";
const VAR_MAX_PASTE_EXPIRATION: &str = "WASTEBIN_MAX_PASTE_EXPIRATION";

#[derive(thiserror::Error, Debug)]
pub enum Error {
Expand All @@ -44,6 +45,13 @@ pub enum Error {
SigningKey(String),
#[error("failed to parse {VAR_HTTP_TIMEOUT}: {0}")]
HttpTimeout(ParseIntError),
#[error("failed to parse {VAR_MAX_PASTE_EXPIRATION}: {0}")]
MaxPasteExpiration(ParseIntError),
#[error(
"{VAR_MAX_PASTE_EXPIRATION} is too large (max {}), pass -1 if you mean no expiry",
u32::MAX
)]
ExpirationTooLarge,
}

pub struct BasePath(String);
Expand Down Expand Up @@ -183,3 +191,17 @@ pub fn http_timeout() -> Result<Duration, Error> {
)
.map_err(Error::HttpTimeout)
}

pub fn max_paste_expiration() -> Result<Option<u32>, Error> {
std::env::var(VAR_MAX_PASTE_EXPIRATION)
.ok()
.and_then(|raw_max_exp| -> Option<Result<u32, Error>> {
match raw_max_exp.parse::<i64>() {
Ok(max_exp) if max_exp == -1 => None,
Ok(max_exp) if max_exp >= u32::MAX as i64 => Some(Err(Error::ExpirationTooLarge)),
Ok(max_exp) => Some(Ok(max_exp as u32)),
Err(why) => Some(Err(Error::MaxPasteExpiration(why))),
}
})
.transpose()
}
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct AppState {
cache: Cache,
key: Key,
base_url: Option<Url>,
max_expiration: Option<u32>,
}

impl FromRef<AppState> for Key {
Expand Down Expand Up @@ -91,13 +92,16 @@ async fn start() -> Result<(), Box<dyn std::error::Error>> {
let max_body_size = env::max_body_size()?;
let base_url = env::base_url()?;
let timeout = env::http_timeout()?;
let max_expiration = env::max_paste_expiration()?;

let cache = Cache::new(cache_size);
let db = Database::new(method)?;
let state = AppState {
db,
cache,
key,
base_url,
max_expiration,
};

tracing::debug!("serving on {addr}");
Expand Down
60 changes: 60 additions & 0 deletions src/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,77 @@ impl From<crate::Error> for ErrorResponse<'_> {
pub struct Index<'a> {
meta: &'a env::Metadata<'a>,
base_path: &'static env::BasePath,
max_expiration: Option<u32>,
}

impl<'a> Default for Index<'a> {
fn default() -> Self {
Self {
meta: env::metadata(),
base_path: env::base_path(),
// exception should already have been handled in main
max_expiration: env::max_paste_expiration().unwrap(),
}
}
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Expiration {
None,
Burn,
Time(u32),
}

impl std::fmt::Display for Expiration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expiration::None => write!(f, ""),
Expiration::Burn => write!(f, "burn"),
Expiration::Time(t) => write!(f, "{}", t),
}
}
}

const EXPIRATION_OPTIONS: [(&'static str, Expiration); 8] = [
("never", Expiration::None),
("10 minutes", Expiration::Time(600)),
("1 hour", Expiration::Time(3600)),
("1 day", Expiration::Time(86400)),
("1 week", Expiration::Time(604800)),
("1 month", Expiration::Time(2592000)),
("1 year", Expiration::Time(31536000)),
("🔥 after reading", Expiration::Burn),
];

impl<'a> Index<'a> {
fn expiry_options(&self) -> String {
let mut option_set = String::new();
let mut wrote_first = false;

option_set.push('\n');

for (opt_name, opt_val) in EXPIRATION_OPTIONS {
if self.max_expiration.is_none()
|| opt_val == Expiration::Burn
|| matches!((self.max_expiration, opt_val), (Some(exp), Expiration::Time(time)) if time <= exp)
{
option_set.push_str("<option");
if !wrote_first {
option_set.push_str(" selected");
wrote_first = true;
}
option_set.push_str(" value=\"");
option_set.push_str(opt_val.to_string().as_ref());
option_set.push_str("\">");
option_set.push_str(opt_name);
option_set.push_str("</option>\n");
}
}

option_set
}
}

/// Paste view showing the formatted paste as well as a bunch of links.
#[derive(Template)]
#[template(path = "formatted.html")]
Expand Down
6 changes: 6 additions & 0 deletions src/routes/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ pub async fn insert(

let url_with_base = base_path().join(&url);

if let Some(max_exp) = state.max_expiration {
entry.expires = entry
.expires
.map_or_else(|| Some(max_exp), |value| Some(value.min(max_exp)));
}

state.db.insert(id, entry).await?;

let jar = jar.add(Cookie::new("uid", uid.to_string()));
Expand Down
8 changes: 7 additions & 1 deletion src/routes/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ pub async fn insert(
.map_err(Error::from)?
.into();

let entry: write::Entry = entry.into();
let mut entry: write::Entry = entry.into();

if let Some(max_exp) = state.max_expiration {
entry.expires = entry
.expires
.map_or_else(|| Some(max_exp), |value| Some(value.min(max_exp)));
}

let url = id.to_url_path(&entry);
let path = base_path().join(&url);
Expand Down
1 change: 1 addition & 0 deletions src/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub(crate) fn make_app() -> Result<Router, Box<dyn std::error::Error>> {
cache,
key,
base_url,
max_expiration: None,
};

Ok(crate::make_app(4096, Duration::new(30, 0)).with_state(state))
Expand Down
10 changes: 2 additions & 8 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,8 @@
<input type="search" id="filter" placeholder="Filter ..." onchange="filterLangs(event);" onkeyup="filterLangs(event)"></input>
</div>
<div class="expiration-list">
<select name="expires" size="7">
<option selected="" value="">never</option>
<option value="600">10 minutes</option>
<option value="3600">1 hour</option>
<option value="86400">1 day</option>
<option value="604800">1 week</option>
<option value="215308800">1 year</option>
<option value="burn">🔥 after reading</option>
<select name="expires" size="8">
{{- Self::expiry_options(self)|safe }}
</select>
</div>
<div class="password">
Expand Down

0 comments on commit 000d30f

Please sign in to comment.