commit
0baa38f138
4 changed files with 128 additions and 32 deletions
20
LICENSE.md
20
LICENSE.md
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 Romain Calascibetta
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
65
README.md
65
README.md
|
@ -0,0 +1,65 @@
|
||||||
|
# An HTTP (http/1.1 or h2) client for MirageOS
|
||||||
|
|
||||||
|
This little library provides an HTTP client which can be usable inside a
|
||||||
|
unikernel/[MirageOS][mirage]. It follows the same API as
|
||||||
|
[http-lwt-client][http-lwt-client] which is pretty simple and uses:
|
||||||
|
- [happy-eyeballs][happy-eyeballs] to resolve domain-name
|
||||||
|
- [ocaml-tls][ocaml-tls] for the TLS layer
|
||||||
|
- [paf][paf] for the HTTP protocol
|
||||||
|
|
||||||
|
This library wants to be easy to use and it is associated to a MirageOS
|
||||||
|
_device_ in order to facilite `functoria` to compose everything (mainly the
|
||||||
|
TCP/IP stack) according to the user's target and give a _witness_ so as to
|
||||||
|
be able to allocate a new connection to a peer and process the HTTP flow.
|
||||||
|
|
||||||
|
## How to use it?
|
||||||
|
|
||||||
|
First, you need to describe a new `http_client` device:
|
||||||
|
```ocaml
|
||||||
|
open Mirage
|
||||||
|
|
||||||
|
type http_client = HTTP_client
|
||||||
|
let http_client = typ HTTP_client
|
||||||
|
|
||||||
|
let http_client =
|
||||||
|
let connect _ modname = function
|
||||||
|
| [ _pclock; _tcpv4v6; ctx ] ->
|
||||||
|
Fmt.str {ocaml|%s.connect %s|ocaml} modname ctx
|
||||||
|
| _ -> assert false in
|
||||||
|
impl ~connect "Http_mirage_client.Make"
|
||||||
|
(pclock @-> tcpv4v6 @-> git_client @-> http_client)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, you can decide how to construct such device:
|
||||||
|
```ocaml
|
||||||
|
let stack = generic_stackv4v6 default_network
|
||||||
|
let dns = generic_dns_client stack
|
||||||
|
let tcp = tcpv4v6_of_stackv4v6 stack
|
||||||
|
|
||||||
|
let http_client =
|
||||||
|
(* XXX(dinosaure): it seems unconventional to use [git_happy_eyeballs] here
|
||||||
|
when we want to do HTTP requests only. The name was not so good and we
|
||||||
|
will fix that into the next release of the mirage tool. But structurally,
|
||||||
|
you don't bring anything related to Git. It's just a bad choice of name. *)
|
||||||
|
let happy_eyeballs = git_happy_eyeballs stack dns
|
||||||
|
(generic_happy_eyeballs stack dns) in
|
||||||
|
http_client $ default_posix_clock $ tcp $ happy_eyeballs
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, you can use the _witness_ into your `unikernel.ml`:
|
||||||
|
```ocaml
|
||||||
|
open Lwt.Infix
|
||||||
|
|
||||||
|
module Make (HTTP_client : Http_mirage_client.S) = struct
|
||||||
|
let start http_client =
|
||||||
|
Http_mirage_client.one_request http_client "https://mirage.io/"
|
||||||
|
>>= function
|
||||||
|
| Ok (resp, body) -> ...
|
||||||
|
| Error _ -> ...
|
||||||
|
end
|
||||||
|
|
||||||
|
[mirage]: https://mirage.io/
|
||||||
|
[happy-eyeballs]: https://github.com/roburio/happy-eyeballs
|
||||||
|
[ocaml-tls]: https://github.com/mirleft/ocaml-tls
|
||||||
|
[paf]: https://github.com/dinosaure/paf-le-chien
|
||||||
|
[http-lwt-client]: https://github.com/roburio/http-lwt-client
|
|
@ -5,16 +5,23 @@ let tls_config = Mimic.make ~name:"tls-config"
|
||||||
|
|
||||||
open Lwt.Infix
|
open Lwt.Infix
|
||||||
|
|
||||||
|
type t =
|
||||||
|
{ ctx : Mimic.ctx
|
||||||
|
; alpn_protocol : Mimic.flow -> string option
|
||||||
|
; authenticator : (X509.Authenticator.t, [ `Msg of string ]) result }
|
||||||
|
|
||||||
module type S = sig
|
module type S = sig
|
||||||
val connect : Mimic.ctx -> Mimic.ctx Lwt.t
|
type nonrec t = t
|
||||||
val alpn_protocol : Mimic.flow -> string option
|
|
||||||
val authenticator : (X509.Authenticator.t, [> `Msg of string ]) result
|
val connect : Mimic.ctx -> t Lwt.t
|
||||||
end
|
end
|
||||||
|
|
||||||
module Make
|
module Make
|
||||||
(Pclock : Mirage_clock.PCLOCK)
|
(Pclock : Mirage_clock.PCLOCK)
|
||||||
(TCP : Tcpip.Tcp.S)
|
(TCP : Tcpip.Tcp.S)
|
||||||
(Happy_eyeballs : Mimic_happy_eyeballs.S with type flow = TCP.flow) : S = struct
|
(Happy_eyeballs : Mimic_happy_eyeballs.S with type flow = TCP.flow) : S = struct
|
||||||
|
type nonrec t = t
|
||||||
|
|
||||||
module TCP = struct
|
module TCP = struct
|
||||||
include TCP
|
include TCP
|
||||||
type endpoint = Happy_eyeballs.t * string * int
|
type endpoint = Happy_eyeballs.t * string * int
|
||||||
|
@ -60,23 +67,6 @@ module Make
|
||||||
let tls_edn, tls_protocol =
|
let tls_edn, tls_protocol =
|
||||||
Mimic.register ~name:"tls" (module TLS)
|
Mimic.register ~name:"tls" (module TLS)
|
||||||
|
|
||||||
let connect ctx =
|
|
||||||
let k0 happy_eyeballs http_scheme http_hostname http_port = match http_scheme with
|
|
||||||
| "http" -> Lwt.return_some (happy_eyeballs, http_hostname, http_port)
|
|
||||||
| _ -> Lwt.return_none in
|
|
||||||
let k1 happy_eyeballs http_scheme http_hostname http_port tls_config = match http_scheme with
|
|
||||||
| "https" -> Lwt.return_some (happy_eyeballs, tls_config, http_hostname, http_port)
|
|
||||||
| _ -> Lwt.return_none in
|
|
||||||
let ctx = Mimic.fold tcp_edn
|
|
||||||
Mimic.Fun.[ req Happy_eyeballs.happy_eyeballs
|
|
||||||
; req http_scheme; req http_hostname; dft http_port 80 ]
|
|
||||||
~k:k0 ctx in
|
|
||||||
Lwt.return (Mimic.fold tls_edn
|
|
||||||
Mimic.Fun.[ req Happy_eyeballs.happy_eyeballs
|
|
||||||
; req http_scheme; req http_hostname; dft http_port 443
|
|
||||||
; req tls_config ]
|
|
||||||
~k:k1 ctx)
|
|
||||||
|
|
||||||
let alpn_protocol flow =
|
let alpn_protocol flow =
|
||||||
let module M = (val (Mimic.repr tls_protocol)) in
|
let module M = (val (Mimic.repr tls_protocol)) in
|
||||||
match flow with
|
match flow with
|
||||||
|
@ -89,6 +79,24 @@ module Make
|
||||||
let authenticator =
|
let authenticator =
|
||||||
let module V = Ca_certs_nss.Make (Pclock) in
|
let module V = Ca_certs_nss.Make (Pclock) in
|
||||||
V.authenticator ()
|
V.authenticator ()
|
||||||
|
|
||||||
|
let connect ctx =
|
||||||
|
let k0 happy_eyeballs http_scheme http_hostname http_port = match http_scheme with
|
||||||
|
| "http" -> Lwt.return_some (happy_eyeballs, http_hostname, http_port)
|
||||||
|
| _ -> Lwt.return_none in
|
||||||
|
let k1 happy_eyeballs http_scheme http_hostname http_port tls_config = match http_scheme with
|
||||||
|
| "https" -> Lwt.return_some (happy_eyeballs, tls_config, http_hostname, http_port)
|
||||||
|
| _ -> Lwt.return_none in
|
||||||
|
let ctx = Mimic.fold tcp_edn
|
||||||
|
Mimic.Fun.[ req Happy_eyeballs.happy_eyeballs
|
||||||
|
; req http_scheme; req http_hostname; dft http_port 80 ]
|
||||||
|
~k:k0 ctx in
|
||||||
|
let ctx = Mimic.fold tls_edn
|
||||||
|
Mimic.Fun.[ req Happy_eyeballs.happy_eyeballs
|
||||||
|
; req http_scheme; req http_hostname; dft http_port 443
|
||||||
|
; req tls_config ]
|
||||||
|
~k:k1 ctx in
|
||||||
|
Lwt.return { ctx; alpn_protocol; authenticator; }
|
||||||
end
|
end
|
||||||
|
|
||||||
module Version = Httpaf.Version
|
module Version = Httpaf.Version
|
||||||
|
@ -283,7 +291,7 @@ let single_request ~ctx ~alpn_protocol ?config cfg ~meth ~headers ?body uri =
|
||||||
Mimic.close flow >|= fun () ->
|
Mimic.close flow >|= fun () ->
|
||||||
r
|
r
|
||||||
|
|
||||||
let tls_config ?tls_config ?config authenticator =
|
let tls_config ?tls_config ?config authenticator user's_authenticator =
|
||||||
lazy ( match tls_config with
|
lazy ( match tls_config with
|
||||||
| Some cfg -> Ok (`Custom cfg)
|
| Some cfg -> Ok (`Custom cfg)
|
||||||
| None ->
|
| None ->
|
||||||
|
@ -291,7 +299,10 @@ let tls_config ?tls_config ?config authenticator =
|
||||||
| None -> [ "h2"; "http/1.1" ]
|
| None -> [ "h2"; "http/1.1" ]
|
||||||
| Some (`H2 _) -> [ "h2" ]
|
| Some (`H2 _) -> [ "h2" ]
|
||||||
| Some (`HTTP_1_1 _) -> [ "http/1.1" ] in
|
| Some (`HTTP_1_1 _) -> [ "http/1.1" ] in
|
||||||
Result.map (fun authenticator -> `Default (Tls.Config.client ~alpn_protocols ~authenticator ())) authenticator )
|
match authenticator, user's_authenticator with
|
||||||
|
| Ok authenticator, None -> Ok (`Default (Tls.Config.client ~alpn_protocols ~authenticator ()))
|
||||||
|
| _, Some authenticator -> Ok (`Default (Tls.Config.client ~alpn_protocols ~authenticator ()))
|
||||||
|
| (Error _ as err), None -> err )
|
||||||
|
|
||||||
let resolve_location ~uri ~location =
|
let resolve_location ~uri ~location =
|
||||||
match String.split_on_char '/' location with
|
match String.split_on_char '/' location with
|
||||||
|
@ -310,15 +321,14 @@ let resolve_location ~uri ~location =
|
||||||
let one_request
|
let one_request
|
||||||
?config
|
?config
|
||||||
?tls_config:cfg
|
?tls_config:cfg
|
||||||
~ctx
|
{ ctx; alpn_protocol; authenticator; }
|
||||||
~alpn_protocol
|
?authenticator:user's_authenticator
|
||||||
~authenticator
|
|
||||||
?(meth= `GET)
|
?(meth= `GET)
|
||||||
?(headers= [])
|
?(headers= [])
|
||||||
?body
|
?body
|
||||||
?(max_redirect= 5)
|
?(max_redirect= 5)
|
||||||
?(follow_redirect= true) uri =
|
?(follow_redirect= true) uri =
|
||||||
let tls_config = tls_config ?tls_config:cfg ?config authenticator in
|
let tls_config = tls_config ?tls_config:cfg ?config authenticator user's_authenticator in
|
||||||
if not follow_redirect
|
if not follow_redirect
|
||||||
then single_request ~ctx ~alpn_protocol ?config tls_config ~meth ~headers ?body uri
|
then single_request ~ctx ~alpn_protocol ?config tls_config ~meth ~headers ?body uri
|
||||||
else
|
else
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
type t
|
||||||
|
|
||||||
module type S = sig
|
module type S = sig
|
||||||
val connect : Mimic.ctx -> Mimic.ctx Lwt.t
|
type nonrec t = t
|
||||||
val alpn_protocol : Mimic.flow -> string option
|
|
||||||
val authenticator : (X509.Authenticator.t, [> `Msg of string ]) result
|
val connect : Mimic.ctx -> t Lwt.t
|
||||||
end
|
end
|
||||||
|
|
||||||
module Make
|
module Make
|
||||||
|
@ -22,9 +24,8 @@ type response =
|
||||||
val one_request :
|
val one_request :
|
||||||
?config:[ `H2 of H2.Config.t | `HTTP_1_1 of Httpaf.Config.t ] ->
|
?config:[ `H2 of H2.Config.t | `HTTP_1_1 of Httpaf.Config.t ] ->
|
||||||
?tls_config:Tls.Config.client ->
|
?tls_config:Tls.Config.client ->
|
||||||
ctx:Mimic.ctx ->
|
t ->
|
||||||
alpn_protocol:(Mimic.flow -> string option) ->
|
?authenticator:X509.Authenticator.t ->
|
||||||
authenticator:(X509.Authenticator.t, [> `Msg of string ]) result ->
|
|
||||||
?meth:Httpaf.Method.t ->
|
?meth:Httpaf.Method.t ->
|
||||||
?headers:(string * string) list ->
|
?headers:(string * string) list ->
|
||||||
?body:string ->
|
?body:string ->
|
||||||
|
|
Loading…
Reference in a new issue