Library
Module
Module type
Parameter
Class
Class type
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.
Mehari provides several packages:
Mehari
provides the core abstraction, it does not depend on any platform code, and does not interact with the environment.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.
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.
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.
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
.
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
...
```
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
.
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"] ()
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.
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
]
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 "/");
]
This road is limited to 3 accesses every 5 minutes:
let limit = Mehari_eio.make_rate_limit ~period:3 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
.
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
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
.