paf

HTTP/AF and MirageOS
README

This library wants to provide an easy way to use HTTP/AF & H2 into a unikernel.
It implements the global /loop/ with a protocol implementation. The code, due to
the compatibility with MirageOS, can work for unix too.

The protocol implementation is given by mimic and should be the
mirage-tcpip implementation.

It does the composition between the TLS encryption layer and the
StackV4V6 implementation to provide a way to initiate a TLS
server. Via mimic, it still keeps the abstraction of the underlying
TCP/IP connection used.

module Make (Stack : Mirage_stack.V4V6) = struct
  module Paf = Paf_mirage.Make (Stack.TCP)

  let start stack =
    let* t = Paf.init ~port:80 (Stack.tcp stack) in
    let service = Paf.http_service ~error_handler request_handler in
    let `Initialized th = Paf.serve service t in
    th
end

(* For UNIX with mirage-time-unix & tcpip.stack-socket *)

include Make (Tcpip_stack_socket.V4V6.TCP)

let stack () =
  let open Tcpip_stack_socket.V4V6 in
  TCP.connect ~ipv4_only:false ~ipv6_only:false
    Ipaddr.V4.Prefix.global None

let () = Lwt_main.run ((Tcpip_stack_socket.V4V6.tcp stack) () >>= start)

It also provides a client-side with the logic of mimic and let the user to
implement the resolution process to determine if the connection needs the TLS
encryption (and how) layer or not.

Mimic

Paf wants to provide an agnostic implementation of HTTP with the ability to
launch a server or a client from an user-defined context: a Mimic.ctx. It
does not exist one and unique way to use Paf because the context can be:

  • a MirageOS

  • a simple executable

  • something else like a JavaScript script (with js_of_ocaml)

Mimic ensures the ability to gives a Mirage_flow.S to Paf
(client side). The underlying implementation of this /flow/ depends on what the
user wants. It can be:

All of these choices is not done by Paf but must be defined by the user.
Then, the CoHTTP layer trusts on mirage-tcpip and
ocaml-tls to easily communicate with a peer from a given Uri.t.
Even if it seems to be the easy way to do HTTP requests (over TLS or not), the
user is able to choose some others possibilities/paths.

For example, the user is able to start a connection with an Unix domain socket:

module Unix_domain_socket : Mimic.Mirage_protocol.S
  with type flow = Unix.file_descr
   and type endpoint = Fpath.t

let unix_domain_socket =
  Mimic.register ~name:"unix-domain-socket" (module Unix_domain_socket)

let ctx =
  Mimic.add unix_domain_socket 
    (Fpath.v "/var/my_domain.sock") Mimic.empty
    
let run =
  Mimic.resolve ~ctx >>= function
  | Error _ as err -> Lwt.return err
  | Ok flow ->
    let body, conn = Httpaf.Client_connection.request ?config:None req
      ~error_handler ~response_handler in
    Paf.run (module Httpaf.Client_connection) conn flow >>= fun () ->
    Lwt.return_ok body

CoHTTP layer

Paf comes with a not-fully-implemented compatible layer with CoHTTP. From this
sub-package and the letsencrypt package, Paf provides a process
to download a Let's encrypt TLS certificate ready to launch an HTTPS server.

let cfg =
  { LE.email= Result.to_option (Emile.of_string "romain@x25519.net")
  ; LE.account_seed= None
  ; LE.account_key_type= `ED25519
  ; LE.account_key_bits= None
  ; LE.certificate_seed= None
  ; LE.certificate_key_type= `ED25519
  ; LE.certificate_key_bits= None
  ; LE.hostname= Domain_name.(host_exn (of_string_exn "x25519.net")) }

let ctx = ... (* see [mimic] *)

module Paf = Paf_mirage.Make (Time) (Tcpip_stack_socket.V4V6)

let get_tls_certificate stack =
  Lwt_switch.with_switch @@ fun stop ->
  let* t = Paf.init ~port:80 stack in
  let service = Paf.http_service
    ~error_handler
    (fun _ -> LE.request_handler) in
  let `Initialized th = Paf.serve ~stop service in
  let fiber =
    LE.provision_certificate ~production:false cfg
      (LE.ctx ~gethostbyname ~authenticator) >>= fun res ->
    Lwt_switch.turn_off stop >>= fun () -> Lwt.return res in
  Lwt.both (th, fiber) >>= fun (_, tls) -> Lwt.return tls

Application Layer Protocol Negotiation

Paf provides the logic behind ALPN negotiation according a certain TLS/SSL
implementation. In other words, Paf is able to correctly dispatch which
protocol the client wants without a requirement of ocaml-tls or
lwt_ssl. The module Alpn is a HTTP service which handles:

  • HTTP/1.1

  • H2

Alpn requires:

  • the accept and the close function

  • a way to extract the result of the Application Layer Protocol Negotiation

  • the Mimic's injection

  • error_handler and request_handler which handle HTTP/1.0, HTTP/1.1 and
    H2 requests

Here is an example with HTTP (without TLS):

let _, protocol
  : Unix.sockaddr Mimic.value
    * (Unix.sockaddr, Lwt_unix.file_descr) Mimic.protocol
  = Mimic.register ~name:"lwt-tcp" (module TCP)

let accept t =
  Lwt.catch begin fun () ->
    Lwt_unix.accept >>= fun (socket, _) ->
    Lwt.return_ok socket
  end @@ function
  | Unix.Unix_error (err, f, v) ->
    Lwt.return_error (`Unix (err, f, v))
  | exn -> raise exn

let info =
  let module R = (val Mimic.register protocol) in
  { Alpn.alpn= const None
  ; Alpn.peer= (fun socket ->
    sockaddr_to_string (Lwt_unix.getpeername socket))
  ; Alpn.injection=
    (fun socket -> R.T socket) }

let service = Alpn.service info handler
  accept Lwt_unix.close

let fiber =
  let t = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
  Lwt_unix.bind t (Unix.ADDR_INET (Unix.inet_addr_loopback, 8080))
  >>= fun () ->
  let `Initialized th = Paf.serve
    service t in th

let () = Lwt_main.run fiber

Tests & Benchmark

The distribution comes with a tool which launch several clients to communicate
with a server. We record the time spent for each request and show as the result
the histogram of them. It's not really a benchmark as is but it a good
stress-test and we check that we don't have failure from the server.

Install
Published
04 Oct 2022
Sources
paf-0.2.0.tbz
sha256=4f3851c454cf90b300b0b3e5d0e342f034612511acb45e7cf9044a6c6b896551
sha512=9099f013c18d7cb019bac334ac29d56d6d3966cc301bcbf52653dcb1e9fde7f8213b7ea6be1189a9b0b19167314b4580f84ecc8478c7ac830dba34b78b7829ee
Dependencies
cstruct
>= "6.0.0"
tls
>= "0.15.0"
faraday
>= "0.7.2"
h2
>= "0.9.0"
httpaf
>= "0.7.1"
bigstringaf
>= "0.7.0"
alcotest-lwt
with-test
uri
with-test
ptime
with-test
fmt
with-test
logs
with-test
base-unix
with-test
lwt
with-test
ke
>= "0.4"
mimic
>= "0.0.5"
tls-mirage
>= "0.15.0"
mirage-time
>= "2.0.0"
tcpip
>= "7.0.0"
dune
>= "2.0.0"
ocaml
>= "4.08.0"
Reverse Dependencies
git-paf
>= "3.5.0" & < "3.7.0" | >= "3.10.0"
paf-cohttp
= "0.2.0"
paf-le
= "0.2.0"