Skip to content

Commit

Permalink
cache-control defaults, pick-random
Browse files Browse the repository at this point in the history
  • Loading branch information
rubyroobs committed Oct 9, 2024
1 parent f0a0bfc commit aabcae8
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ log = { version = "0.4.4" }
mime_guess = "2.0.5"
mio = { version = "0.8", features = ["net", "os-poll"] }
pki-types = { package = "rustls-pki-types", version = "1", features = ["std"] }
rand = "0.8.5"
rcgen = { version = "0.13", features = ["pem", "aws_lc_rs"], default-features = false }
rustls = { version = "0.23.4", features = [ "logging" ]}
rustls-pemfile = "2"
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ The below flow is provided as a reference for how `rubyshd` routes requests, as
- Try `{PUBLIC_ROOT_PATH}/path.gmi`
- Try `{PUBLIC_ROOT_PATH}/path.gmi.hbs`

All HTTPS responses for static files (i.e. everything except rendered templates/redirects/errors) are marked as cacheable with the `max-age` value set to `CACHEABLE_MAX_AGE_SECONDS`.

### Templates

The [`handlebars-rust`](https://github.com/sunng87/handlebars-rust) project is used for templating and the original [handlebarsjs.com](https://handlebarsjs.com/) documentation is a sufficient reference. However, these `rubyshd`-specific decorators/helpers/quirks are useful to know. Unless otherwise stated, this applies to requests from both the HTTPS and Gemini protocols.
Expand All @@ -79,6 +81,7 @@ The [`handlebars-rust`](https://github.com/sunng87/handlebars-rust) project is u
* The `*status` decorator can be used to set the status code used for the response. The value in the last call to the decorator will be the one used. The parameter must be one of the `Status` slugs in `src/response.rs`. For example, `{{*status "unauthenticated"}}` and `{{*status "other_server_error"}}` are valid calls.
* The `*media-type` decorator can be used to set the response media type (i.e. `Content-Type` in HTTPS responses). For example, `{{*media-type "text/csv"}}` and `{{*media-type "application/json"}}` are valid calls.
* The `*temporary-redirect` and `*permanent-redirect` decorators can be used to set temporary and permanent redirects respectively. For example, `{{*temporary-redirect "https://google.com/"}}` will return a temporary redirect to `https://google.com`. For consistency with Gemini, no response body will be returned with HTTPS responses when a redirect is made regardless of it's position in the template (templates will always render in full unless an error occurs).
* The `pick-random` helper takes an array and chooses a random value from it. For example, if `random_photos.json` contains an array of random photo URLs, `pick-random data.random_photos` will return one of the values from the array.
* The following request-specific properties are also available:
* `peer_addr` - client IP address
* `path` - the requested path
Expand Down
1 change: 1 addition & 0 deletions src/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ fn try_load_file(path: &str, request: &Request) -> Result<Response, Status> {
.first_raw()
.unwrap_or(&request.protocol().media_type()),
&body,
true,
)),
Err(_) => Err(Status::Unauthorized),
};
Expand Down
12 changes: 12 additions & 0 deletions src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use tokio::net::TcpStream;
use tokio_rustls::server::TlsStream;
use url::Url;

const CACHEABLE_MAX_AGE_SECONDS: u16 = 14_400;

struct HttpHeaderEntry {
name: String,
value: String,
Expand Down Expand Up @@ -113,6 +115,16 @@ impl Protocol {
name: "Content-Type".to_string(),
value: response.media_type().to_string(),
});

let cache_max_age = match response.cacheable() {
true => CACHEABLE_MAX_AGE_SECONDS,
false => 0,
};

headers.push(HttpHeaderEntry {
name: "Cache-Control".to_string(),
value: format!("public, max-age={}, must-revalidate", cache_max_age),
});
}

headers.push(HttpHeaderEntry {
Expand Down
11 changes: 10 additions & 1 deletion src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,17 @@ pub struct Response {
media_type: String,
redirect_uri: String,
body: Vec<u8>,
cacheable: bool,
}

impl Response {
pub fn new(status: Status, media_type: &str, body: &[u8]) -> Response {
pub fn new(status: Status, media_type: &str, body: &[u8], cacheable: bool) -> Response {
Response {
status: status,
media_type: media_type.to_string(),
redirect_uri: "".to_string(),
body: body.to_vec(),
cacheable: cacheable,
}
}

Expand All @@ -79,6 +81,7 @@ impl Response {
media_type: "".to_string(),
redirect_uri: redirect_uri.to_string(),
body: Vec::new(),
cacheable: false,
}
}

Expand All @@ -98,6 +101,7 @@ impl Response {
media_type: response.media_type().to_owned(),
redirect_uri: "".to_string(),
body: response.body().to_vec(),
cacheable: false,
}
}
Err(_) => {}
Expand All @@ -121,6 +125,7 @@ impl Response {
Status::OtherClientError => "Other client error",
}
.into(),
cacheable: false,
}
}

Expand All @@ -139,4 +144,8 @@ impl Response {
pub fn body(&self) -> &[u8] {
&self.body
}

pub fn cacheable(&self) -> bool {
self.cacheable
}
}
45 changes: 42 additions & 3 deletions src/templates.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use log::error;
use rand::seq::{IteratorRandom as _, SliceRandom};
use std::env;
use std::net::SocketAddr;
use std::str::FromStr;

use handlebars::{
to_json, Context, Decorator, Handlebars, Helper, HelperResult, JsonRender, Output,
RenderContext, RenderError, RenderErrorReason,
to_json, Context, Decorator, Handlebars, Helper, HelperDef, HelperResult, JsonRender, Output,
RenderContext, RenderError, RenderErrorReason, ScopedJson,
};

use crate::protocol::Protocol;
Expand Down Expand Up @@ -39,6 +40,7 @@ pub fn initialize_handlebars(handlebars: &mut Handlebars) {
"private-context-serialize",
Box::new(serialize_context_helper),
);
handlebars.register_helper("pick-random", Box::new(pick_random_helper));
handlebars.register_decorator("temporary-redirect", Box::new(temporary_redirect_decorator));
handlebars.register_decorator("permanent-redirect", Box::new(permanent_redirect_decorator));
handlebars.register_decorator("status", Box::new(status_decorator));
Expand Down Expand Up @@ -117,7 +119,12 @@ pub fn render_response_body_for_request(
};

match response_context.redirect_uri {
None => Ok(Response::new(status, &media_type, rendered_body.as_bytes())),
None => Ok(Response::new(
status,
&media_type,
rendered_body.as_bytes(),
false,
)),
Some(redirect_uri) => {
Ok(Response::new_with_redirect_uri(status, &redirect_uri))
}
Expand Down Expand Up @@ -166,6 +173,38 @@ fn serialize_context_helper(
Ok(())
}

#[allow(non_camel_case_types)]
pub struct pick_random_helper;

impl HelperDef for pick_random_helper {
fn call_inner<'reg: 'rc, 'rc>(
&self,
h: &Helper<'rc>,
_: &'reg Handlebars,
_: &'rc Context,
_: &mut RenderContext<'reg, 'rc>,
) -> Result<ScopedJson<'reg>, RenderError> {
let param = h
.param(0)
.ok_or(RenderErrorReason::ParamNotFoundForIndex("pick-random", 0))?;

if let Some(array) = param.value().as_array() {
match array.choose(&mut rand::thread_rng()) {
Some(value) => Ok(ScopedJson::Derived(value.clone())),
None => Ok(ScopedJson::Derived(serde_json::Value::Null)),
}
} else if let Some(object) = param.value().as_object() {
match object.values().choose(&mut rand::thread_rng()) {
Some(value) => Ok(ScopedJson::Derived(value.clone())),
None => Ok(ScopedJson::Derived(serde_json::Value::Null)),
}
} else {
// TODO: raise an invalid param error here?
Ok(ScopedJson::Derived(serde_json::Value::Null))
}
}
}

fn status_decorator<'reg: 'rc, 'rc>(
d: &Decorator,
_: &Handlebars,
Expand Down

0 comments on commit aabcae8

Please sign in to comment.