192 lines
6.6 KiB
Markdown
192 lines
6.6 KiB
Markdown
## Paf le chien - A MirageOS compatible layer for [HTTP/AF][httpaf] and [H2][h2]
|
|
|
|
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][mimic] and should be the
|
|
[mirage-tcpip][mirage-tcpip] implementation.
|
|
|
|
It does the composition between the TLS encryption layer and the
|
|
[StackV4V6][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.
|
|
|
|
```ocaml
|
|
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][mirage-flow] to Paf
|
|
(client side). The underlying implementation of this /flow/ depends on what the
|
|
user wants. It can be:
|
|
- [ocaml-tls][ocaml-tls]
|
|
- [lwt_ssl][lwt_ssl]
|
|
- [mirage-tcpip][mirage-tcpip]
|
|
- The host TCP/IP stack (see the `Unix` module)
|
|
|
|
All of these choices **is not** done by Paf but must be defined by the user.
|
|
Then, the CoHTTP layer trusts on [mirage-tcpip][mirage-tcpip] and
|
|
[ocaml-tls][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:
|
|
|
|
```ocaml
|
|
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][letsencrypt] package, Paf provides a process
|
|
to download a Let's encrypt TLS certificate ready to launch an HTTPS server.
|
|
|
|
```ocaml
|
|
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][ocaml-tls] or
|
|
[lwt_ssl][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):
|
|
```ocaml
|
|
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.
|
|
|
|
[httpaf]: https://github.com/inhabitedtype/httpaf
|
|
[mimic]: https://github.com/mirage/ocaml-git
|
|
[mirage-tcpip]: https://github.com/mirage/mirage-tcpip
|
|
[letsencrypt]: https://github.com/mmaker/ocaml-letsencrypt
|
|
[stackv4v6]: https://github.com/mirage/mirage-stack
|
|
[ocaml-tls]: https://github.com/mirleft/ocaml-tls
|
|
[lwt_ssl]: https://github.com/ocsigen/lwt_ssl
|
|
[mirage-flow]: https://github.com/mirage/mirage-flow
|
|
[h2]: https://github.com/anmonteiro/ocaml-h2
|