Vif, a simple webserver in OCaml
Vif is a small program that allows you to initiate a web server (http/1.1 & h2) from an OCaml script:
$ opam install vif
$ cat >main.ml <<EOF
#require "vif" ;;
open Vif ;;
let default req _server () =
let str = "Hello World!\n" in
let* () = Response.with_string req str in
Response.respond `OK
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
[ get (rel /?? nil) --> default ]
;;
let () = Miou_unix.run @@ fun () ->
Vif.run routes ()
;;
EOF
$ vif main.ml --pid vif.pid &
[1] 1337
$ curl http://localhost:8080/
Hello World!
$ kill -SIGINT $(cat vif.pid)
The aim is to have a small web server for cheap and to be able to iterate on it
quickly without necessarily having a workflow that involves compiling and
executing an OCaml project. The OCaml script is executed by vif
just as it
could be executed by ocaml
and it describes what your webserver should do.
In this short tutorial, we will see how to use Vif to implement more or less complex web services.
Vif as a library
Of course, vif is also a library. You don't need to write an OCaml script to get a web server. You can also develop your own application and compile it with Vif to get an executable that will be your web server.
However, in terms of iterations, the loop ocaml script → web server test is faster than ocaml program → compilation → web server test.
How to install Vif
Vif is available on opam. You can install it using the command:
$ opam install vif
You can also obtain the development version of Vif via:
$ opam pin add https://git.robur.coop/robur/vif.git
The Vif project is hosted on our server git.robur.coop. You can also access a mirror on GitHub. Issues and pull requests can be proposed on both repositories (pull requests will be merged into our cooperative's repository).
My first Vif application
To understand Vif, we will iterate several times on a small project whose goal
is to implement a website with a user area. This goal will allow us to see the
main features of Vif. To do this, we will also use hurl
, an HTTP client in
OCaml that uses the same libraries as Vif. The latter will allow us to inspect
a whole host of details regarding requests, responses and cookies.
Let's start by installing vif
and hurl
using opam
. Next, we will create a
folder and work in it to develop our brand new website.
$ opam install vif
$ opam install hurl
$ mkdir my-first-vif-application
$ cd my-first-vif-application
Vif is a library, but we also offer a small program that allows you to run an OCaml script and launch an HTTP server. This approach has the advantage of avoiding the "development, compilation, testing" loop (as is the case with native languages) and focusing primarily on development and testing.
We will come back to the vif
tool, the library and the different ways of using
this project in more detail later, but for now let's focus on developing our
website.
My first webpage with Vif
We are therefore going to create an OCaml script in which we will develop our website:
$ cat >main.ml <<EOF
#require "vif" ;;
let () = Miou_unix.run @@ fun () -> Vif.run [] () ;;
EOF
$ vif --pid vif.pid main.ml &
$ hurl http://localhost:8080/
HTTP/1.1 404 Not Found
connection: close
content-length: 120
content-length: 120
content-type: text/plain; charset=utf-8
Unspecified destination / (GET):
user-agent: hurl/0.0.1
host: localhost
connection: close
content-length: 0
$ kill -SIGINT $(cat vif.pid)
Here, we write a new file called main.ml
, which will simply launch the
Miou scheduler and the Vif server. We then run this script using vif
and make an HTTP request using hurl
to obtain a response. Finally, we kill
the Vif server.
The Vif server responded with a 404 (not found) response because no routes are defined, and it also describes the request it just received. We will therefore add a route for our website.
#require "vif" ;;
let index req _server () =
let open Vif.Response.Syntax in
let str = "Hello World!\n" in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `OK
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
[ get (rel /?? any) --> index ]
;;
let () = Miou_unix.run @@ fun () ->
Vif.run routes () ;;
$ vif --pid vif.pid main.ml &
$ hurl https://localhost:8080/
HTTP/1.1 200 OK
connection: close
content-length: 13
content-type: text/plain; charset=utf-8
Hello World!
$ kill -SIGINT $(cat vif.pid)
And here is our first page with Vif! We have added a new index
function that
will process the request and provide a response. In this response, we will
write "Hello World!"
and send it with the code 200 (`OK
). We can see
that this is indeed what the server responds when we use hurl
.
This index
function will be associated with a route get (rel /?? any)
. This
route allows us to filter requests and specify that the index
function will
only process GET requests with the path "/"
.
Routes
The principle behind routes is fairly simple to understand: it allows you to
associate (-->
) a path on your website with a function that will process the
request and respond. What Vif brings to the table is the ability to type
routes. Another frequently requested route feature is the ability to specify
holes in the path, which would be values provided by the user.
For example, we would like a route in which the user could specify the name:
#require "vif" ;;
let hello req (name : string) _server () =
let open Vif.Response.Syntax in
let str = Fmt.str "Hello %S!\n" name in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `OK
;;
let user req (uid : int) _server () =
let open Vif.Response.Syntax in
let str = Fmt.str "User %d!\n" uid in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `OK
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
[ get (rel / "hello" /% string `Path /?? any) --> hello
; get (rel / "user" /% int /?? any) --> user ]
;;
let () = Miou_unix.run @@ fun () ->
Vif.run routes () ;;
$ hurl http://localhost:8080/hello/robur -p b
Hello "robur"!
$ hurl http://localhost:8080/user/42 -p b
User 42!
$ hurl http://localhost:8080/user/robur -p b
Unspecified destination /user/robur (GET):
...
As we can see, it is possible to define "holes" in our routes with Vif and
obtain their value in the functions associated with these routes. It is also
possible to type these holes so that you only process a certain type of
information, such as an integer (as is the case for our second user
function).
Vif will then not only recognise integers, but also transform the value into a
real OCaml integer that you can manipulate.
It is possible to define more complex "holes" that must match a regular expression. Here is an example where we would like to recognise certain fruits in our route.
#require "vif" ;;
type fruit =
| Apple
| Orange
| Banana
;;
let pp ppf = function
| Apple -> Fmt.string ppf "Apple"
| Orange -> Fmt.string ppf "Orange"
| Banana -> Fmt.string ppf "Banana"
;;
let fruit =
let v = Tyre.regex Re.(alt [ str "apple"; str "orange"; str "banana" ]) in
let inj = function
| "apple" -> Apple
| "orange" -> Orange
| "banana" -> Banana
| _ -> assert false in
let prj = function
| Apple -> "apple"
| Orange -> "orange"
| Banana -> "banana" in
Vif.Uri.conv inj prj v
;;
let like req (fruit : fruit) _server () =
let open Vif.Response.Syntax in
let str = Fmt.str "I like %a!\n" pp fruit in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `OK
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
[ get (rel /% fruit /?? any) --> like ]
;;
let () = Miou_unix.run @@ fun () ->
Vif.run routes () ;;
$ hurl http://localhost:8080/orange -p b
I like Orange!
$ hurl http://localhost:8080/cherries -p b
Unspecified destination /cherries (GET):
...
Here we describe how to recognise certain fruits using a regular expression
(and thanks to the re
library). Next, we describe how to convert
recognised strings into OCaml values (inj
). Finally, we use this new value in
our route in order to recognise only the specified fruits.
Next steps
At this point, you should have a basic understanding of how routes work with Vif. The next step is to create our user space. This essentially consists of two routes:
- a route for submitting user credentials to the server
- a route reserved for logged-in users
First, we will focus on the server-side mechanics of credential verification. These credentials will initially be submitted using JSON.
Next, we will introduce the concept of cookies and middleware, which will allow us to create our second route (accessible only to logged-in users).
Finally, we will enhance this foundation with other features offered by Vif.
My basic userspace
Vif implements the basics of web frameworks. It offers middleware and cookie features, among others. In this chapter, we will use Vif's routing system to see how to implement a user space. We will essentially create two routes:
- a route that allows users to submit their login details
- a route that should only be accessible to users
As for the first route, since it involves submitting identifiers, it will be a POST route. In the previous chapter, it was possible to type routes, but Vif is also capable of typing the content of requests. In this case, we will create a POST route that expects content in JSON format (which will be the user's identifiers).
jsont
& Vif
Vif uses the jsont
library to obtain a DSL for describing
information in JSON format. The aim here is to describe a type (the
identifiers) and describe its equivalent in JSON format using jsont
.
type credentials =
{ username : string
; password : string }
;;
let credentials =
let open Jsont in
let username = Object.mem "username" string in
let password = Object.mem "password" string in
let fn username password =
{ username; password } in
Object.map fn
|> username
|> password
|> Object.finish
;;
Thanks to the credentials
value, we can now serialise and deserialise JSON
and obtain an OCaml value of type credentials
. Vif is capable of handling
this type of value in order to deserialise the content of a request itself as
soon as it recognises the content type application/json
.
let login req _server () =
let open Vif.Response.Syntax in
match Vif.Request.of_json req with
| Ok (v : credentials) ->
let str = Fmt.str "username: %S, password: %S\n"
v.username v.password in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `OK
| Error (`Msg msg) ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Invalid JSON: %s\n" msg in
let* () = Vif.Response.with_string req str in
Vif.Response.respond (`Code 422)
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
let open Vif.Type in
[ post (json_encoding credentials) (rel /?? nil) --> login ]
;;
let () = Miou_unix.run @@ fun () ->
Vif.run routes () ;;
Here's how to send a JSON request using hurl
:
$ hurl -m POST http://localhost:8080/ username=robur password=42 -p b
username: "robur", password: "42"
$ hurl -m POST http://localhost:8080/ foo=bar
HTTP/1.1 422
connection: close
content-length: 122
content-type: text/plain; charset=utf-8
Invalid JSON: Missing members in object:
password
username
In this example, we have specified that our route expects content in JSON
format. Next, Vif uses jsont
to attempt to deserialise the given JSON to the
OCaml value credentials
. If it fails, we return code 422; otherwise, we
display the information.
The goal here is that serialisation and deserialisation can be seen as a fairly
repetitive task for the user. In this case, Vif handles two formats: JSON using
jsont
and multipart/form-data
, which we will see in another chapter. These
are the two most commonly used formats with the HTTP protocol, and Vif therefore
handles these formats natively, providing you with a simple way to deserialise
them into OCaml values.
Vif and cookies
When a user logs in, we want to keep the information on the user's side so that they remain logged in. We therefore want to store this information and also secure it. We will use Vif to create a new cookie, which will contain a JSON Web Token (JWT) that ensures the information is secure.
The JWT requires a secret, a value that only the server knows in order to encrypt/decrypt the JWT. This is where we introduce three concepts:
- Vif's ability to load an external library (we will use jwto, an implementation of JWT in OCaml)
- the ability to obtain configuration values (often found in the
.env
file of a website) - the creation of a new cookie if the user has logged in successfully
#require "jwto" ;;
type env =
{ secret : string }
;;
let verify { username; password } =
match username, password with
| "robur", "42" -> true
| _ -> false
;;
let login req server { secret } =
let open Vif.Response.Syntax in
match Vif.Request.of_json req with
| Ok ({ username; _ } as v) when verify v ->
let token = Jwto.encode HS512 secret [ "username", username ] in
let token = Result.get_ok token in
let* () = Vif.Cookie.set ~name:"my-token" server req token in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Authenticated!\n" in
Vif.Response.respond `OK
| Ok _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Unauthorized!\n" in
Vif.Response.respond `Unauthorized
| Error (`Msg msg) ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Invalid JSON: %s\n" msg in
let* () = Vif.Response.with_string req str in
Vif.Response.respond (`Code 422)
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
let open Vif.Type in
[ post (json_encoding credentials) (rel / "login" /?? nil) --> login ]
;;
let () = Miou_unix.run @@ fun () ->
let env = { secret= "deadbeef" } in
Vif.run routes env ;;
As you can see, it is quite simple to load a new library; just use the
require
directive. Next, define a new env
type that will contain the secret
needed to generate JWTs.
We implement a fairly simple function verify
that recognises a single user
with their password.
Finally, we modify the login
function to verify the information provided by
the client. If the credentials are correct, we generate a JWT token using our
secret
passed to our login
function and use Vif.Cookie.set
to add a new
cookie.
This is where you can specify the last argument of the Vif.run
function. This
value will be passed to all your handlers. Its main purpose is to allow
developers to define their own type containing global information such as the
secret used to generate JWTs. Of course, you can extend the env
type. It is
often compared to .env
found in several web frameworks.
This is now the result when attempting to log in:
$ hurl -m POST http://localhost:8080/login username=robur password=21 -p rb
HTTP/1.1 401 Unauthorized
Unauthorized!
$ hurl -m POST http://localhost:8080/login username=robur password=42
HTTP/1.1 200 OK
connection: close
content-length: 15
content-type: text/plain; charset=utf-8
set-cookie: __Host-my-token=ALeeJoP8W2KfmX9oYcHMnjeJDuGJmV67brUluoEJgHLZWHEk...
Path=/; Secure; HttpOnly; SameSite=Lax
Authenticated!
We are well connected! We can clearly see the cookie that the client should
save. If we take a step back from the code, what we are doing here is managing
a POST request containing identifiers, checking that they are correct, and
creating a JWT using jwto
if they are. In this token, we will simply store
the user's username
. We then need to inform the client that we would like to
save this token on their end. We do this using Vif.Cookie.set
(there are a
whole host of options, but I'll leave you to discover them in the
documentation).
As a client, we can see the cookie (with Set-Cookie
). It is encrypted...
twice! Once by Vif itself (you can — and should — specify the encryption key
with Vif.config) and once by jwto
.
We can now introduce a new concept in Vif: middleware.
Vif and middleware
Middleware is a simple function that applies to all requests (whether or not there is a defined route). Within this function, it is possible to inspect the request, particularly the header. However, it is impossible to:
- inspect the content of the request
- send a response
The purpose of middleware is to transform and/or add information from the header of the incoming request. For example, it is possible to analyse the cookies sent by the user and determine whether or not they were able to log in. Finally, this information is added to the request.
After the middleware, the handler associated with the route can finally process the request to which the middleware has added information. This information can be retrieved and a response can be sent accordingly.
The goal here would be to have middleware that attempts to deserialise the JWT
present in the cookies. If it succeeds, it means that the person has logged in
previously. The user's username
is included in this token, which allows us to
identify them. Here is how to create middleware using Vif.
type user =
{ username : string }
;;
let jwt =
Vif.Middleware.v ~name:"jwt" @@ fun req target server { secret } ->
match Vif.Cookie.get server req ~name:"my-token" with
| Error _err -> None
| Ok token ->
let ( let* ) = Option.bind in
let token = Jwto.decode_and_verify secret token in
let* token = Result.to_option token in
let* username = List.assoc_opt "username" (Jwto.get_payload token) in
Some { username }
;;
let index req server _env =
let open Vif.Response.Syntax in
match Vif.Request.get jwt req with
| None ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Unauthorized!\n" in
Vif.Response.respond `Unauthorized
| Some { username } ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Connected as %S!\n" username in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `OK
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
let open Vif.Type in
[ post (json_encoding credentials) (rel / "login" /?? nil) --> login
; get (rel /?? nil) --> index ]
;;
let () = Miou_unix.run @@ fun () ->
let env = { secret= "deadbeef" } in
let middlewares = Vif.Middlewares.[ jwt ] in
Vif.run ~middlewares routes env ;;
Here, we create a new middleware called jwt
. Its purpose is to search for a
cookie called my-token
(the one we created in the login
handler) and decode
(and verify) the JWT. If successful, we should obtain the username of the
current user. We will simply add this information to the current request and
return Some <data>
.
In our new index
handler, we will try to see if our middleware has returned
the information correctly. If so, it means that the client has a JWT and is
logged in. All we need to do is display the username!
Finally, we need to tell Vif that we want to apply this middleware.
Note that the middleware also has access to our env
and therefore also has
access to our secret
value (required for jwto
). The user can also specify
the type of value that the middleware is capable of creating. In our case, we
have defined the type user
containing only the field username
. We can of
course extend this type with other values such as the user's ID, age, etc. In
short, Vif allows you to define your own types.
You can now test our website and its user area:
$ curl -X POST http://localhost:8080/login \
--header 'Content-Type: application/json' \
--data "{ \"username\": \"robur\", \"password\": \"42\" }" \
--cookie-jar cookies.txt
Authenticated!
$ curl http://localhost:8080/ --cookie cookies.txt
Connected as "robur"!
$ curl http://localhost:8080/
Unauthorized!
Et voilà! A user space created using Vif. This allowed us to explore the concepts of cookies, JWTs, and middleware.
At this stage, we have most of what a framework can offer. We can manage incoming information (and type it) as specified in our routes or in the request content, and process this information while allowing developers to specify their own types.
The main idea is that OCaml, being a typed language, has the advantage of
characterising information (at least, much more than a simple string
). We can
use ADTs or records, which are much easier to use. It is then essentially a
matter of defining how to convert untyped information into OCaml values.
Next steps
We now have a solid foundation for improving our website. In the next chapter,
we will propose a way to display an HTML page for logging in, which will allow
us to introduce a new format: multipart/form-data
. Next, we will see how to
generate HTML dynamically depending on whether the user is logged in or not.
This will allow us to introduce new concepts such as ppx
and static files.
My enhanced userspace
We have seen the basic elements for our user space. However, our website is still quite limited, as it assumes that the user knows how to send a POST request via the command line. In this chapter, we will improve our user space so that:
- we have an HTML page with a login form
- we can handle the
multipart/form-data
format rather than the JSON format - we have a dynamic HTML page depending on whether the user is logged in or not
Static HTML files (and conan
)
When creating a website, we often want to deliver static content such as images or HTML pages. When sending these documents to the client, important information is sent along with them: the MIME type.
This informs the user of the type of content being sent (e.g. whether it is a
*.png
or *.jpg
image).
With this in mind, we have developed software that can recognise the MIME type
of any file: the conan
project. Recognition is not based on the file
extension, but rather on the content and a database that references most MIME
types.
$ conan.file --mime login.html
text/html
Next, for Vif, we need to add a handler that, depending on the path given by
the request, not only attempts to find the associated static file, but also
recognises its MIME type using conan
to deliver it to the client. Let's start
with this HTML form:
<html>
<body>
<form action="/login" method="post" enctype="multipart/form-data">
<label for="username">Enter your username: </label>
<input type="text" name="username" required />
<label for="password">Enter your password: </label>
<input type="password" name="password" required />
<input type="submit" value="Login!" />
</form>
</body>
</html>
Since the route for delivering a static file depends on the filename, we will introduce a new concept to Vif: default handlers. When Vif cannot find any routes for a request, it will execute a series of default handlers until one produces a response.
For static files, Vif already provides such a handler: Vif.Handler.static
.
Just add it to Vif.run
:
let () = Miou_unix.run @@ fun () ->
let env = { secret= "deadbeef" } in
let middlewares = Vif.Middlewares.[ jwt ] in
let handlers = [ Vif.Handler.static ?top:None ] in
Vif.run ~handlers ~middlewares routes env ;;
The top
value corresponds to the location of the static files (so that Vif
prohibits access to other files deeper in the directory tree). In addition, our
login.html
file is in the same folder as our main.ml
file (however, it is
recommended to create an appropriate folder containing only static files).
$ hurl http://localhost:8080/login.html
HTTP/1.1 200 OK
transfer-encoding: chunked
etag: 3b8eae63b7baa4a7c24bfd8ee7600ee4b97306064e9ea336fca949011058a559
content-length: 356
content-type: text/html
<html>
<body>
<form action="/login" method="post" enctype="multipart/form-data">
<label for="username">Enter your username: </label>
<input type="text" name="username" required />
<label for="password">Enter your password: </label>
<input type="password" name="password" required />
<input type="submit" value="Login!" />
</form>
</body>
</html>
We are now able to deliver static files! Note the appearance of ETag
. This is
information that allows the client to cache the file (and avoid re-downloading
the content). Finally, note that conan
has correctly recognised the MIME type
of our file.
MIME-type and conan
MIME file recognition is quite difficult and is a topic in itself (in terms of
performance and ability to recognise strange files). Even though conan
finds
quite a few solutions, it may happen that we are unable to recognise the file
type. There are other, less obvious ways to transfer the contents of a file
using Vif (such as Vif.Response.with_file
), where you can specify the MIME
type manually.
It is also possible to improve conan
(and its database) to recognise a subset
or larger set of files. We will leave it up to the user to choose the best
solution for their context.
Vif & multipart/form-data
In our form, we specify that we would like to transfer the information in
multipart/form-data
format. This is a somewhat special format that is often
used for websites. Fortunately, there is an implementation in OCaml that Vif
uses: multipart_form.
Vif extends this library so that it is as easy to use as jsont
: we therefore
offer another DSL for describing the format of your forms:
let login_form =
let open Vif.Multipart_form in
let fn username password =
{ username; password } in
record fn
|+ field "username" string
|+ field "password" string
|> sealr
;;
let login req server { secret } =
let open Vif.Response.Syntax in
match Vif.Request.of_multipart_form req with
| Ok ({ username; _ } as v) when login v ->
let token = Jwto.encode HS512 secret [ "username", username ] in
let token = Result.get_ok token in
let str = "Authenticated!\n" in
let* () = Vif.Cookie.set ~name:"my-token" server req token in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `OK
| Ok _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Unauthorized!\n" in
Vif.Response.respond `Unauthorized
| Error _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Invalid multipart/form-data\n" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond (`Code 422)
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
let open Vif.Type in
[ post (m login_form) (rel / "login" /?? nil) --> login
; get (rel /?? nil) --> index ]
;;
The idea is almost the same as with jsont
. We just need to redefine our login
route with m
instead of json_encoding
so that Vif accepts requests with a
Content-Type
of multipart/form-data
.
$ hurl -m POST http://localhost:8080/login --multipart username=robur password=42 -p b
Authenticated!
Vif, tyxml
and ppx
It would now be more useful to transmit HTML content rather than simple text. Several solutions are available, but we will use one in particular that emphasises the typed generation of an HTML document using TyXML:
#require "tyxml-ppx" ;;
#require "tyxml" ;;
open Tyxml ;;
let%html index username = {html|
<html>
<head><title>My Vif Website!</title></head>
<body>
<p>Hello |html} username {html| !</p>
</body>
</html>
|html} ;;
let index username : Tyxml_html.doc = index [ Html.txt username ] ;;
let index req server _env =
let open Vif.Response.Syntax in
match Vif.Request.get jwt req with
| None ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Unauthorized!\n" in
Vif.Response.respond `Unauthorized
| Some { username } ->
let* () = Vif.Response.with_tyxml req (index username) in
Vif.Response.respond `OK
;;
What's interesting here is that TyXML will check the HTML content to see if it
complies with standards. Furthermore, if you remove the <head>
tag, for
example, TyXML will complain that this mandatory tag is missing.
This is not, of course, the only way to deliver content to the client. We can
also respond with JSON as plain text, as we have done since the beginning of
this book. This example essentially shows that it is possible to use ppx
within a Vif script.
Vif & redirections
One last small detail, but one that may be important: users should now be
redirected to the main page if they manage to log in, rather than receiving a
message saying that they are logged in. Vif offers a redirect_to
function
that allows you to redirect the user to a given Vif.Uri.t
.
let login req server { secret } =
let open Vif.Response.Syntax in
match Vif.Request.of_multipart_form req with
| Ok ({ username; _ } as v) when login v ->
let token = Jwto.encode HS512 secret [ "username", username ] in
let token = Result.get_ok token in
let str = "Authenticated!\n" in
let* () = Vif.Cookie.set ~name:"my-token" server req token in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req str in
Vif.Response.redirect_to req Vif.Uri.(rel /?? any)
| Ok _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Unauthorized!\n" in
Vif.Response.respond `Unauthorized
| Error _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Invalid multipart/form-data\n" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond (`Code 422)
;;
We can now test our user space with a single command (since hurl
handles
redirection and cookies):
$ curl http://localhost:8080/login -F username=robur -F password=42 -L --cookie-jar cookies.txt
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>My Vif Website!</title></head>
<body>
<p>Hello robur !</p>
</body>
</html>
We're starting to get somewhere. We'll continue using the command line, but we can now test our website with our web browser! We're producing HTML content, and all we need to do is log in to http://localhost:8080/login.html to be redirected to our main page!
Next steps
At this point, we can consider that the hardest part is done. However, we would
surely like to go further and, in particular, communicate with a database
rather than our simple verify
function. In the next chapter, we will see how
to extend our user space so that we can use a persistent database to which our
users can register.
A persistent userspace
Vif is therefore a web framework, but it does not provide any means of communicating with a database. However, when developing a website, it is essential to be able to store information (such as user data) persistently and independently of our HTTP server. Fortunately, another project allows communication with a database: caqti.
In this chapter, we will see how to communicate with a database using caqti
so that our users' information is stored outside the HTTP server and in a
persistent manner. To simplify this chapter, we will use sqlite3: a
simple file that represents our database (note that caqti
can communicate
with other types of databases such as PostGreSQL).
Create & use a database
Here we will create a simple database with a table called users
containing
the user's username and password.
$ sqlite3 vif.db <<EOF
CREATE TABLE users (uid INTEGER, username TEXT, password TEXT, PRIMARY KEY(uid));
.quit
EOF
Next, we need to modify our Vif script so that we can read this database. To do
this, we will use caqti-miou
, the caqti
support with our Miou scheduler. We
need to install it:
$ opam install caqti-miou caqti-driver-sqlite3
Next, we will need to explain to Vif how to create an instance that can
communicate with our database. Vif uses the concept of devices, which are
global instances available from our server
instance and therefore
available in all our request handlers.
These devices have the particularity of being domain-safe, meaning that two domains can request these devices in parallel. We therefore need to ensure that their manipulation is also domain-safe.
In this case, caqti-miou
creates what is called a connection pool to the
database. There is only one database, but several handlers can process SQL
requests in parallel (and require a connection to the database). Thanks to
caqti
, we can obtain a CONNECTION
to the database (in a domain-safe
manner) and, from this connection, make an SQL query (such as SELECT
).
So let's first see how to create a Vif device and how to use it:
#require "caqti-miou" ;;
#require "caqti-miou.unix" ;;
#require "caqti-driver-sqlite3" ;;
type env =
{ sw : Caqti_miou.Switch.t
; uri : Uri.t
; secret : string }
;;
let caqti =
let finally pool = Caqti_miou_unix.Pool.drain pool in
Vif.Device.v ~name:"caqti" ~finally [] @@ fun { sw; uri; _ } ->
match Caqti_miou_unix.connect_pool ~sw uri with
| Error err -> Fmt.failwith "%a" Caqti_error.pp err
| Ok pool -> pool
;;
let users req server _ =
let pool = Vif.Server.device caqti server in
let sql =
let open Caqti_request.Infix in
Caqti_type.(unit ->! int) "SELECT COUNT(uid) FROM users" in
let fn (module Conn : Caqti_miou.CONNECTION) = Conn.find sql () in
match Caqti_miou_unix.Pool.use fn pool with
| Ok n ->
let open Vif.Response.Syntax in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset= utf-8" in
let* () = Vif.Response.with_string req (Fmt.str "%d user(s)!\n" n) in
Vif.Response.respond `OK
| Error err ->
let open Vif.Response.Syntax in
let str = Fmt.str "SQL error: %a" Caqti_error.pp err in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `Internal_server_error
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
let open Vif.Type in
[ post (m form) (rel / "login" /?? nil) --> login
; get (rel /?? nil) --> index
; get (rel / "users" /?? nil) --> users ]
;;
let () =
Miou_unix.run @@ fun () ->
Caqti_miou.Switch.run @@ fun sw ->
let uri = Uri.make ~scheme:"sqlite3" ~path:"vif.db" () in
let env = { sw; uri; secret= "deadbeef" } in
let middlewares = Vif.Middlewares.[ jwt ] in
let handlers = [ Vif.Handler.static ?top:None ] in
let devices = Vif.Devices.[ caqti ] in
Vif.run ~devices ~handlers ~middlewares routes env
;;
Here, we extend our env
type to include the caqti
switch and the uri
to
our database. We then create our caqti
device, which we will finally pass to
Vif.run
(via the devices
argument).
The users
handler is an example of an SQL query that counts the number of
users. As you can see, we can retrieve our connection pool via
Vif.Server.device caqti
. Finally, we need to use this connection pool to make
an SQL query, but we suggest you refer to the caqti
documentation.
$ hurl http://localhost:8080/users
HTTP/1.1 200 OK
connection: close
content-length: 11
content-type: text/plain; charset= utf-8
0 user(s)!
Our database is empty, but this query confirms that we did indeed run an SQL query to find out that it is empty! The client connected to our Vif server, the server connected to our database, retrieved the information, processed it, and then responded in text format that there are zero users.
Verify passwords
We will add a new user manually and improve our login
function so that it
uses our database:
$ echo -n "42" | sha256sum
73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049 -
$ sqlite3 vif.db <<EOF
INSERT INTO users (username, password) VALUES ('robur', '73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049');
.quit
EOF
$ hurl http://localhost:8080/users -p b
1 user(s)!
Here, we can clearly see that the addition of a new user is taken into account
by our Vif server. It should be noted that we do not need to restart our Vif
server to obtain this response. This information is now stored in the database,
and our Vif server will simply (and each time) request the number of users. Now
let's re-implement the login
function:
#require "digestif.c" ;;
#require "digestif" ;;
type user =
{ uid : int
; username : string
; password : Digestif.SHA256.t }
;;
let user =
let open Caqti_template.Row_type in
let sha256 =
let encode hash = Ok (Digestif.SHA256.to_hex hash) in
let decode hex = Ok (Digestif.SHA256.of_hex hex) in
custom ~encode ~decode string in
product (fun uid username password -> { uid; username; password })
@@ proj int (fun (t : user) -> t.uid)
@@ proj string (fun (t : user) -> t.username)
@@ proj sha256 (fun (t : user) -> t.password)
@@ proj_end
;;
let login server (c : credentials) =
let pool = Vif.Server.device caqti server in
let sql =
let open Caqti_request.Infix in
Caqti_type.(string ->* user)
"SELECT * FROM users WHERE username = ?" in
let fn (module Conn : Caqti_miou.CONNECTION) = Conn.collect_list sql c.username in
match Caqti_miou_unix.Pool.use fn pool with
| Ok [ user ] ->
let h = Digestif.SHA256.digest_string c.password in
Digestif.SHA256.equal h user.password
| _ -> false
;;
let login req server { secret } =
let open Vif.Response.Syntax in
match Vif.Request.of_multipart_form req with
| Ok (({ username; _ } : credentials) as c) when login server c ->
let token = Jwto.encode HS512 secret [ "username", username ] in
let token = Result.get_ok token in
let* () = Vif.Cookie.set ~name:"my-token" server req token in
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Authenticated!\n" in
Vif.Response.redirect_to req Vif.Uri.(rel /?? any)
| Ok _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Unauthorized!\n" in
Vif.Response.respond `Unauthorized
| Error _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Invalid multipart/form-data\n" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond (`Code 422)
;;
We can see that caqti
offers (using Caqti_template
) more or less the same
DSL that we used with jsont
or multipart_form
. Using this DSL, we can
describe how to deserialise an SQL value to an OCaml value, which is what we do
with the user
type. Next, we modify our first login
function so that it
makes the SQL query to search for our user.
Passwords are hashed using the SHA256 algorithm (thanks to the digestif
library). We will therefore hash the value given by the user and compare it
with what we have in the database.
Finally, the second login
function changes very little; we just need to
change when verify c
to when login server c
so that our function can obtain
the caqti
connection pool.
$ curl http://localhost:8080/login -F username=robur -F password=42 \
--cookie-jar cookies.txt -L
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>My Vif Website!</title></head>
<body><p>Hello robur !</p></body>
</html>
We now have a user space that obtains user information from a database!
We would like to emphasise that throughout, we have attempted to type all
information (the password is no longer a string but a hash, which is a type
offered by digestif
). The results of SQL queries are also typed
using Caqti. The main idea is always to prefer OCaml values (which have
undergone a whole series of checks upstream) to basic values that require a
whole ceremony to validate them (and, above all, to avoid bugs).
We will now focus on creating a new user. However, at this stage, I believe you should be able to create such a page on your own.
Create a new user
We will now implement a new page that will be a registration form. This page
will be associated with a POST route that will add the new user to our database
and redirect the client to our index page. We will reuse everything we have
just learned here. Let's start with the registration page registration.html
:
<html>
<body>
<form action="/subscribe" method="post" enctype="multipart/form-data">
<label for="username">Enter your username: </label>
<input type="text" name="username" required />
<label for="password">Enter your password: </label>
<input type="password" name="password" required />
<input type="submit" value="Subscribe!" />
</form>
</body>
</html>
Next, we will create a new POST route to register the user.
let subscribe req server _ =
let pool = Vif.Server.device caqti server in
let ( let* ) = Result.bind in
let search =
let open Caqti_request.Infix in
Caqti_type.(string ->* user)
"SELECT * FROM users WHERE username = ?" in
let insert =
let open Caqti_request.Infix in
Caqti_type.(t2 string string ->. unit)
"INSERT INTO users (username, password) VALUES (?, ?)" in
let already_exists (c : credentials) (module Conn : Caqti_miou.CONNECTION) =
let* user = Conn.collect_list search c.username in
Ok (List.is_empty user == false) in
let insert (c : credentials) (module Conn : Caqti_miou.CONNECTION) =
let hash = Digestif.SHA256.digest_string c.password in
let hash = Digestif.SHA256.to_hex hash in
Conn.exec insert (c.username, hash) in
let result =
let* c = Vif.Request.of_multipart_form req in
let* () =
match Caqti_miou_unix.Pool.use (already_exists c) pool with
| Ok true -> Error (`Already_exists c.username)
| Ok false -> Ok ()
| Error (#Caqti_error.t as err) -> Error err in
match Caqti_miou_unix.Pool.use (insert c) pool with
| Ok () -> Ok ()
| Error (#Caqti_error.t as err) -> Error err in
let open Vif.Response.Syntax in
match result with
| Ok () ->
let* () = Vif.Response.empty in
Vif.Response.redirect_to req Vif.Uri.(rel / "form.html" /?? any)
| Error (`Already_exists username) ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "%S already exists.\n" username in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `Conflict
| Error (#Caqti_error.t as err) ->
let open Vif.Response.Syntax in
let str = Fmt.str "SQL error: %a" Caqti_error.pp err in
let* () = Vif.Response.with_string req str in
Vif.Response.respond `Internal_server_error
| Error (`Invalid_multipart_form | `Not_found _) ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Invalid multipart/form-data\n" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond (`Code 422)
;;
let routes =
let open Vif.Uri in
let open Vif.Route in
let open Vif.Type in
[ post (m login_form) (rel / "login" /?? nil) --> login
; get (rel /?? nil) --> index
; get (rel / "users" /?? nil) --> users
; post (m login_form) (rel / "subscribe" /?? nil) --> subscribe ]
;;
In the code above, we reuse our login_form
value, which describes our form.
This is because the form on the register.html
page is the same as the one on
login.html
.
Next, we check whether the user already exists. We therefore make an initial
SQL query and, if the user does not exist, we make an INSERT
query. Finally,
we handle most error cases.
The code may be longer, but what is really interesting is how the result
value is calculated. If we look more closely, this function essentially
consists of SQL queries and returning errors in certain cases. This is where
Vif lets the developer choose how to organise the project.
One solution is to create a library containing the routes, SQL queries and functions for displaying the results. But all this is outside the scope of Vif, which is primarily intended to facilitate processes specific to managing HTTP requests and producing responses.
We can now test our server:
$ hurl -m POST http://localhost:8080/subscribe --multipart username=foo password=bar
HTTP/1.1 303 See Other
location: /form.html
connection: close
content-length: 0
$ curl http://localhost:8080/login -F username=foo -F password=bar -L \
--cookie-jar cookies.txt
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>My Vif Website!</title></head>
<body><p>Hello foo !</p></body>
</html>
Congratulations! You have created the basics of a user space using Vif. At this point, we recommend that you explore the world of OCaml to see how to develop a real web application. Vif focuses primarily on the server side, but there are of course other aspects of web development that Vif does not handle (often referred to as the front end).
Final steps
We will conclude this tutorial by creating a chat room. Vif is capable of managing websockets. This means that several authenticated clients can communicate with each other in a shared space. This will allow us to explore more complex but equally interesting features of Vif.
The current site can also be improved in many ways. For example, we can add an
email address (validated by emile
) and send a confirmation email
(using sendmail
) to complete the registration process.
We could also add an avatar for our users and allow them to upload an image
that would be validated with conan
and stored on our server.
In short, there are many possible ways to go at this stage, but the Vif documentation is quite comprehensive and will provide you with all the information you need.
A chatroom with websockets and JavaScript
There is one last way to communicate with our HTTP server: the WebSocket protocol. The advantage of this protocol is that the connection is full-duplex: throughout the entire process, the user can communicate with you and you can continuously communicate with them. It's like a persistent communication.
In short, a good example of the use of WebSockets is a chat room. We want users to be able to communicate with other users in real time. We will therefore initiate WebSocket connections between these users in order to multiplex the messages.
First, we will create a fairly basic page where users can send messages to each other:
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" defer="defer" src="chat.js"></script>
<title>Chat room</title>
</head>
<body>
<form id="send">
<input type="text" name="msg" required />
<input type="submit" value="Send!" />
</form>
</body>
</html>
Here we add a file called chat.js
, which will be the result of compiling an
OCaml file using js_of_ocaml
. This small JavaScript script has two objectives:
- connect to the server via WebSocket and receive messages to write on the page
- send messages as soon as the user clicks on the
Send!
button.
To implement all this, we will use brr
, a library that interfaces
JavaScript functions in OCaml:
let form_elem = Brr.El.find_first_by_selector (Jstr.v "#send") |> Option.get
let send_message ev_submit =
let open Fut.Syntax in
let _ = Jv.call (Brr.Ev.to_jv ev_submit) "preventDefault" [||] in
let form_elem = Brr_io.Form.of_el form_elem in
let form_data = Brr_io.Form.Data.of_form form_elem in
let body = Brr_io.Fetch.Body.of_form_data form_data
and credentials = Brr_io.Fetch.Request.Credentials.same_origin in
let init = Brr_io.Fetch.Request.init
~body ~credentials ~method':(Jstr.v "POST") () in
let req = Brr_io.Fetch.Request.v ~init (Jstr.v "http://localhost:8080/send") in
Fut.await begin
let* result = Brr_io.Fetch.request req in
match result with
| Ok resp when Brr_io.Fetch.Response.ok resp -> Fut.return ()
| Ok resp -> print_endline "Error!"; Fut.return ()
| Error _ -> print_endline "Error!"; Fut.return ()
end @@ Fun.id
let on_message ev =
let msg = Jv.Jstr.get (Brr.Ev.to_jv ev) "data" in
let div = Brr.El.(div [txt msg]) in
Brr.El.append_children (Brr.Document.body Brr.G.document) [div]
let () =
let socket = Brr_io.Websocket.create (Jstr.v "http://localhost:8080/chat") in
let target = Brr_io.Websocket.as_target socket in
let event = Brr.Ev.Type.create (Jstr.v "message") in
ignore (Brr.Ev.listen event on_message target);
let target = Brr.El.as_target form_elem in
ignore (Brr.Ev.listen Brr_io.Form.Ev.submit send_message target)
The code may seem complex, and we could write the equivalent in JavaScript, but let's stay in the world of OCaml. The goal here is to retrieve our form and be able to read what the user has written as soon as they press the "Send!" button.
Next, we initiate a WebSocket connection with our server (taking care to keep our cookies so that we can remain authenticated).
Finally, for each message received from the WebSocket, we will simply write it on the fly on the page.
The JavaScript code can be obtained in this way:
$ opam install brr js_of_ocaml
$ ocamlfind c -linkpkg -package brr chat.ml
$ js_of_ocaml a.out -o chat.js
That's all we need on the client side. We will now return to Vif to:
- propose a handler to the WebSocket protocol
- and create a new POST route to send messages
Our listeners on our Vif server
The idea behind our chatroom is quite simple. When someone connects to our server via websocket, we create a listener in the sense that the client will listen for any new messages.
This listener will be stored in a global value so that it can be retrieved from any request handlers. As such, access must be protected so that this global value is domain-safe.
There are three essential operations:
- create a listener
- send a message to all listeners (i.e. all connected clients)
- delete a listener
type chatroom =
{ make : unit -> int * string Vif.Stream.source
; send : string -> unit
; shutdown : int -> unit }
;;
let chatroom =
let uid = Atomic.make 0 in
let actives = Hashtbl.create 0x100 in
let mutex = Miou.Mutex.create () in
let make () =
let n = Atomic.fetch_and_add uid 1 in
Miou.Mutex.protect mutex @@ fun () ->
let q = Vif.Stream.Bqueue.create 0x100 in
Hashtbl.replace actives n q;
n, Vif.Stream.Source.of_bqueue q in
let shutdown uid =
Miou.Mutex.protect mutex @@ fun () ->
Hashtbl.remove actives uid in
let send msg =
Miou.Mutex.protect mutex @@ fun () ->
let fn _ q = Vif.Stream.Bqueue.put q msg in
Hashtbl.iter fn actives in
let finally _ =
Miou.Mutex.protect mutex @@ fun () ->
let fn _ q = Vif.Stream.Bqueue.close q in
Hashtbl.iter fn actives in
Vif.Device.v ~name:"chatroom" ~finally [] @@ fun _ ->
{ make; send; shutdown }
;;
Here, we introduce two new concepts: a bounded queue and streams. We won't go
into detail about these modules, but they allow information to be transmitted
(and it's always domain-safe) between tasks. Conceptually, several tasks
(probably dispatched across several domains) will run to listen for any
messages we might want to send. A task will then appear that will execute the
handler for the POST request (allowing messages to be sent) and will have to
transmit this message to all active listeners (this is the purpose of the
send
function).
Our chatroom, being global to our server, will be a device. We will then create the WebSocket handler and a final route to be able to send messages.
Websocket
The WebSocket protocol is a protocol that can be initiated from an HTTP
request. It involves creating a route and informing the client that we would
like to switch to the WebSocket protocol rather than HTTP, which is called an
upgrade. Vif allows you to attempt this upgrade. The client will then be
redirected to another handler, the WebSocket handler. This handler is special
because it no longer processes a request and provides a response, but works
with a stream of inputs (ic
) and a stream of outputs (oc
).
let chat req server _ = Vif.Response.websocket ;;
let websocket ic oc server _ =
let t = Vif.Server.device chatroom server in
let uid, src = t.make () in
let fn str = oc (`Msg (`Text, true), str) in
let prm0 = Miou.async @@ fun () -> Vif.Stream.Source.each fn src in
let prm1 = Miou.async @@ fun () ->
let rec go () =
match ic () with
| None | Some (`Connection_close, _) -> oc (`Connection_close, String.empty)
| Some _ -> go () in
go () in
let _ = Miou.await_first [ prm0; prm1 ] in
t.shutdown uid
;;
Here, we introduce a few concepts related to Miou. When a client connects to our websocket, the goal is to create tasks that will work together:
- one will consume everything the client can send (and it should not send anything normally)
- the other task will consist of transmitting messages from our listener to our client
Vif.Stream.Source.each
will execute fn
as soon as the listener receives a
message, and fn
will simply write this message to the client using oc
.
One or both of these tasks will stop (because the client has disconnected or
because we want to shutdown the server). What is certain is that in any case,
everything must end. Miou.await_first
will wait for one of the tasks and,
more importantly, will cancel the other. We can finally release the listener
resource correctly.
It may be interesting to look at Miou at this point and how it manages tasks. We can recommend a short book that explains in detail what a scheduler looks like in OCaml 5 (with effects) and what Miou offers.
Send a message!
Here, we will create a new handler, which will be our last one and will
summarise everything we have learned since the beginning of this short book. It
is a handler for a POST request in which we would have our message (msg
). It
is still a question of whether the client is connected, and we will simply
prefix the message with the client's name.
type msg = { msg : string } ;;
let msg =
let open Vif.Multipart_form in
let fn msg = { msg } in
record fn
|+ field "msg" string
|> sealr
;;
let send req server _ =
let open Vif.Response.Syntax in
match Vif.Request.of_multipart_form req, Vif.Request.get jwt req with
| Ok { msg }, Some { username }->
let t = Vif.Server.device chatroom server in
let str = Fmt.str "%s: %s\n" username msg in
t.send str;
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Message sent!\n" in
Vif.Response.respond `OK
| Error _, _ ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let str = Fmt.str "Invalid multipart/form-data\n" in
let* () = Vif.Response.with_string req str in
Vif.Response.respond (`Code 422)
| _, None ->
let* () = Vif.Response.add ~field:"content-type" "text/plain; charset=utf-8" in
let* () = Vif.Response.with_string req "Unauthorized!" in
Vif.Response.respond `Unauthorized
;;
Note that we are reusing our chatroom
device here. This does not involve
creating a listener as in our WebSocket handler, but rather broadcasting the
message to all listeners.
Mix them all!
We now need to properly configure our new chatroom
device in Vif and give it
the handler for WebSocket connections:
let routes =
let open Vif.Uri in
let open Vif.Route in
let open Vif.Type in
[ post (m login_form) (rel / "login" /?? nil) --> login
; get (rel /?? nil) --> index
; get (rel / "users" /?? nil) --> users
; post (m login_form) (rel / "subscribe" /?? nil) --> subscribe
; get (rel / "chat" /?? nil) --> chat
; post (m msg) (rel / "send" /?? nil) --> send ]
;;
let () =
Miou_unix.run @@ fun () ->
Caqti_miou.Switch.run @@ fun sw ->
let uri = Uri.make ~scheme:"sqlite3" ~path:"vif.db" () in
let env = { sw; uri; secret= "deadbeef" } in
let middlewares = Vif.Middlewares.[ jwt ] in
let handlers = [ Vif.Handler.static ?top:None ] in
let devices = Vif.Devices.[ caqti; chatroom ] in
Vif.run ~devices ~handlers ~middlewares ~websocket routes env
;;
There you go! You can now access the page http://localhost:8080/chat.html and, if you are logged in, you can send a message that others will be able to read. It's a real chat room made with Vif.
Conclusion
Of course, there is room for improvement (starting with the design!). But the bulk of the logic, the backend, is there. Despite Vif's minimalism, it is possible to achieve satisfactory results fairly quickly.
Vif offers a way to develop websites with OCaml by taking up the idea of OCaml scripts. Fortunately, this is not the central idea (note the use of effects with Miou, the possibility of parallelising request management with OCaml 5, etc.). The idea of scripting in OCaml is interesting because it requires very little to get a website up and running.
Finally, Vif attempts to offer, at all levels of the HTTP protocol, a way of
typing information so that all checks can be performed upstream using DSLs such
as jsont
or caqti
. The idea is to really take advantage of the OCaml type
system (and see it more as an assistant rather than a constraint).