fluffer 2023.10.27

Fluffer 🦊 is a crate that aims to make writing Gemini apps fun and easy.
Documentation
fluffer-2023.10.27 has been yanked.

🦊 Fluffer

Fluffer is an experimental crate that aims to make writing Gemini apps fun and easy.

🗼 Design

Similar to Axum, Fluffer routes are generic functions that can return anything that implements the [GemBytes] trait.

There are some helpful implementations out of the box, so please consult [GemBytes] and [Fluff] while you experiment.

Also, this crate has a lot of examples for you to check out. Including a dice roller app.

Here is a basic example of a Fluffer app.

use fluffer::{App, Fluff};

#[tokio::main]
async fn main() {
    App::default()
        .route("/", |_| async {
            "# Welcome\n=> /u32 Should show a number\n=> /pic 🦊 Here's a cool picture!"
        })
        .route("/u32", |_| async { 777 })
        .route("/pic", |_| async { Fluff::File("picture.png".to_string()) })
        .run()
        .await;
}

💎 GemBytes

The [GemBytes] trait returns a Gemini byte response, which is formatted like this:

<STATUS><SPACE><META>\r\n<CONTENT>

Note: you must include the <SPACE> character, even if <META> is blank.

To implement [GemBytes] on a type is to decide which Gemini response is appropriate for it.

For example: it is sensible to represent some mime-ambiguous data as a successful Gemtext response so it can be read in a client.

use fluffer::{GemBytes, async_trait};

struct Profile {
    name: String,
    bio: String,
}

#[async_trait]
impl GemBytes for Profile {
    async fn gem_bytes(&self) -> Vec<u8> {
        format!("20 text/gemini\r\n# {},\n{}", self.name, self.bio).into_bytes()
    }
}

📜 Certificates

Server

Fluffer looks for the files ./key.pem (private) and ./cert.pem (public) at runtime. If they can't be located, a prompt appears to generate a key pair interactively.

There's currently no way to define an alternate path to your pem files.

Client identity

Gemini uses client certificates to facilitate identities.

[Context] exposes functions with the ident_ prefix, which correspond to common identity practices in Gemini.

  • [Context::ident_get] gets the client's certificate.
  • [Context::ident_verify] returns true if the current client's certificate matches one you pass.
  • [Context::ident_name] returns the first entry in the certificate's subject_name field. This can be used to provide temporary usernames, or just to say hello.
  • [Context::ident_name] returns the first entry in the certificate's subject_name field. This can be used to provide temporary usernames, or just to say hello.

🥴 Parameters and Input

Queries in Gemini aren't one-to-one with HTTP.

Gemini clients tend to consider the entire query line to be a user's input. As such, they discard any queries you may have included in a link.

In other words, /?p=20 often becomes /?user%20input.

This is a problem for apps like search engines, which may want to include filters and pagination in each request alongside a user's search query.

To simplify the problem, Fluffer encourages you to use the whole query as input, and [matchit]'s route parameters for everything else.

Input

To get a user's input to a route, call [Context::input]. This returns the whole query line percent-decoded.

App::default()
    .route("/" |ctx| async {
        ctx.input().unwrap_or("no input 😥".to_string())
    })
    .run()
    .await
    .unwrap()

Parameters

To access a parameter, you must declare it first in the path string. Referencing an undefined parameter causes the connection's thread to panic.

App::default()
    .route("/page=:number" |ctx| async {
        format!("{}", ctx.parameter("number").unwrap_or("no page number 💢"))
    })
    .run()
    .await
    .unwrap()

If you're unfamiliar with matchit patterns, here's a couple of examples:

  • "/owo/:a/:b" defines parameters a and b, e.g: /owo/thisisa/thisisb
  • "/page=:n/filter=:f defines the parameter n, e.g: /page=20/filter=date.

Things to keep in mind:

  • Every parameter must be included in your url for the route to be found.
  • Be careful where you define your parameters. It's possible to consume requests intended for a different route.
  • It's more flexible to represent complex expressions as a single parameter, which you parse manually inside the route function.

📚 Helpful Resources

📋 Todo

  • Async for route functions
  • Switch to openssl
  • Add peer certificate to context
  • Spawn threads
  • User-defined data
  • Titan support