Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.