package mehari

  1. Overview
  2. Docs

Mehari

Mehari is a cross-platform library for building Gemini servers. It fully implements the Gemini protocol specification. It offers a simple and clean interface to create complete Gemini web apps. It takes heavy inspiration from Dream, a tidy, feature-complete Web framework.

Consult the Tutorial and examples.

Interface

Mehari provides several packages:

  • Mehari provides the core abstraction, it does not depend on any platform code, and does not interact with the environment.
  • Mehari_lwt_unix uses the Lwt library, and specifically the UNIX bindings. Contains also extra features based on UNIX filesystem such as CGI.

Implementation choice

IO implementations of Mehari are roughly equivalent in terms of features. However, some differences exist, e.g. Mehari_eio supports concurrent connections and should be preferred for a high performance server.

Tutorial

In these tutorials, Mehari_eio will be used so as not to complicate the snippets with a monadic interface but they can easily be adapted into a Mehari_mirage version.

Respond to request

The first form of abstraction is the Mehari.NET.handler which is essentially an asynchronous function from Mehari.request to Mehari.response. This is the simplest possible handler that responds to all requests in the same way:

(fun _ -> Mehari.response_text "Hello from Mehari")

As handlers take a Mehari.request in parameter we can operate on them. Here we retrieve the URL of the client's request and return it:

(fun req -> Mehari.uri req |> Uri.to_string |> Mehari.response_text)

See request section to consult all client request related functions.

Status

Mehari provides its own representation of status codes as described in the gemini specification. Take a look at the signature of Mehari.response:

val response : 'a status -> 'a -> response

Furthermore, the status input and success are defined as follows:

val input : string status
val success : body -> mime status

The inhabitant type of Mehari.status carries the information of what is necessary for the creation of an associated response. In the case of an input response, a string is needed:

let input_resp =
  Mehari.(response input) "Enter a message"

In the same way, a Mehari.body and a Mehari.mime are required to make a successful response:

let successful_resp =
  let body = Mehari.string "A successful response" in
  Mehari.(response (success body) (gemini ()))

See status for the complete list of status. Other functions described in response section are mostly convenient functions built on top of Mehari.response.

Body

Successful responses are accompanied by a Mehari.body. They are many methods to create a body, for example from a string:

let body = Mehari.string "A response body"

Or from a Gemtext object:

let body =
  Mehari.(gemtext Gemtext.[
    heading `H1 "Thought on Gemtext markup";
    list_item "Date: February 2021";
    list_item "Tags: gemini, reviews";
    newline;
    heading `H2 "Introduction";
    newline;
    quote "The format permits richer typographic possibilities than the plain text of Gopher, but remains extremely easy to parse.";
    text "Here is an example of a Python parser that demonstrates the truth of that statement:";
    preformat "..." ~alt:"python"
  ])

Which is rendered as the following Gemtext document:

# Thought on Gemtext markup
* Date: February 2021
* Tags: gemini, reviews

## Introduction

> The format permits richer typographic possibilities than the plain text of Gopher, but remains extremely easy to parse.
Here is an example of a Python parser that demonstrates the truth of that statement:
```python
...
```
Data stream response

Mehari offers ways to keep client connections open forever and stream data in real time. Be sure to read this quick warning about this approach: note-on-data-stream-response.

Mime

Mehari.mime describes how the response body must be interpreted by the client. You can build your own mime with Mehari.make_mime and specify the data encoding with the parameter charset:

let mp3 = Mehari.make_mime "audio/mp3"

Some common MIME type are predefined. See mime section.

The text/gemini MIME type allows an additional parameter lang to specify the languages used in the document according to the Gemini specification:

let french_ascii_gemini = Mehari.gemini ~charset:"ascii" ~lang:["fr"] ()
Inference

Mehari.from_filename enables MIME type infering from a filename.

Mehari also provides an experimental Conan integration via Mehari.from_content to infer MIME type from a string.

Routing

Obviously, the path of a route corresponds to the "path" component of the url requested by the client. Currently, two types of route exist: "raw" route which are interpreted literally and route supplied as a Perl style regex. Note that routes are "raw" by default:

Mehari_eio.route "/var/gemini" (fun _ -> ...)

In the following snippet, Mehari.param retrieves the first group of the regex starting from index 1. It is possible to have as many groups as desired in the route path.

Mehari_eio.route ~regex:true "/articles/([a-z][A-Z])+" (fun req ->
  Mehari.param req 1
  |> Printf.sprintf "Get article %S"
  |> Mehari.response_text)

It is the purpose of Mehari.NET.router to group routes together to produce a bigger handler:

Mehari_eio.router [
  Mehari_eio.route "/" index_handler;
  Mehari_eio.route "/gemlog" gemlog_handler
]

In the same way Mehari.NET.scope groups several routes in one under the given prefix:

Mehari_eio.scope "/blog" [
  Mehari_eio.route "/articles" articles_handler;
  Mehari_eio.route "/gemlog" gemlog_handler
]

Advanced routing

Middleware

Mehari.NET.middleware allows to run code before and after the execution of another handler and produces a “bigger” Mehari.NET.handler.

This example of an incrementable counter shows how to use them:

let counter = ref 0

let incr_count handler req =
  incr counter;
  handler req

let router =
  Mehari_eio.router [
      Mehari_eio.route "/" (fun _ ->
        Printf.sprintf "Value %i" !counter |> Mehari.response_text);
      Mehari_eio.route "/incr" ~mw:incr_count (fun _ ->
          Mehari.response Mehari.redirect_temp "/");
    ]

Rate limit

This road is limited to 3 accesses every 5 minutes:

let limit = Mehari_eio.make_rate_limit 5 `Minute

let limited_route = Mehari_eio.route "/stats" ~rate_limit:limit (fun _ -> ...)

They are described in depth in section rate_limit.

In the same way as shown in section Routing, these features can be mutualized at the scale of several routes using Mehari.NET.scope.

Hosting

By default, the server runs on port 1965 on IP localhost (usually 127.0.0.1).

This command generate certificates (cert.pem and key.pem), set server common name to localhost and it should work:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes

Here is what you have to do to start the server:

Using Mehari_lwt_unix:

let ( >>= ) = Lwt.Infix.( >>= )

let main () =
  X509_lwt.private_of_pems ~cert:"cert.pem" ~priv_key:"key.pem" >>= fun cert ->
  |> Mehari_io.run_lwt router ~certchains:[ cert ]

let () = Lwt_main.run (main ())

Using Mehari_eio:

let main ~net ~cwd =
  let certchains =
    Eio.Path.
      [
        X509_eio.private_of_pems ~cert:(cwd / "cert.pem")
          ~priv_key:(cwd / "key.pem");
      ]
  in
  Mehari_eio.run net ~certchains router

let () =
  Eio_main.run @@ fun env ->
  Mirage_crypto_rng_eio.run (module Mirage_crypto_rng.Fortuna) env @@ fun () ->
  main ~net:env#net ~cwd:env#cwd

Virtual hosting

Mehari supports virtual hosting using "server name indication" (SNI). Mehari.NET.virtual_hosts takes a list composed of a couple which represent a domain and his associated handler and produces a "biggest" Mehari.NET.handler.