diff --git a/config.ml b/config.ml index d3d9346..2b98ca3 100644 --- a/config.ml +++ b/config.ml @@ -1,7 +1,8 @@ open Mirage -(* boilerplate from https://github.com/mirage/ocaml-git.git unikernel/config.ml - (commit #3bfcf215f959b71580e5c0b655700bb9484aee8c) *) +(* boilerplate from https://github.com/mirage/ocaml-git.git + file unikernel/empty-commit/config.ml + commit #ecdfc6dc13834f5f1a8e378718512eda6e67c982 *) type mimic = Mimic let mimic = typ Mimic @@ -88,13 +89,13 @@ let mimic_dns_conf = let packages = [ package "git-mirage" ~sublibs:[ "dns" ] ] in impl @@ object inherit base_configurable - method ty = random @-> mclock @-> time @-> stackv4v6 @-> mimic @-> mimic + method ty = random @-> mclock @-> pclock @-> time @-> stackv4v6 @-> mimic @-> mimic method module_name = "Git_mirage_dns.Make" method! packages = Key.pure packages method name = "dns_ctx" method! connect _ modname = function - | [ _; _; _; stack; tcp_ctx ] -> + | [ _; _; _; _; stack; tcp_ctx ] -> Fmt.str {ocaml|let dns_ctx00 = Mimic.merge %s %s.ctx in let dns_ctx01 = %s.with_dns %s dns_ctx00 in @@ -104,34 +105,19 @@ let mimic_dns_conf = | _ -> assert false end -let mimic_dns_impl random mclock time stackv4v6 mimic_tcp = - mimic_dns_conf $ random $ mclock $ time $ stackv4v6 $ mimic_tcp - -type paf = Paf -let paf = typ Paf - -let paf_conf () = - let packages = [ package "paf" ~sublibs:[ "mirage" ] ] in - impl @@ object - inherit base_configurable - method ty = time @-> stackv4v6 @-> paf - method module_name = "Paf_mirage.Make" - method! packages = Key.pure packages - method name = "paf" - end - -let paf_impl time stackv4v6 = paf_conf () $ time $ stackv4v6 +let mimic_dns_impl random mclock pclock time stackv4v6 mimic_tcp = + mimic_dns_conf $ random $ mclock $ pclock $ time $ stackv4v6 $ mimic_tcp let mimic_paf_conf () = let packages = [ package "git-paf" ] in impl @@ object inherit base_configurable - method ty = time @-> pclock @-> stackv4v6 @-> paf @-> mimic @-> mimic + method ty = time @-> pclock @-> stackv4v6 @-> mimic @-> mimic method module_name = "Git_paf.Make" method! packages = Key.pure packages method name = "paf_ctx" method! connect _ modname = function - | [ _; _; _; _; tcp_ctx; ] -> + | [ _; _; _; tcp_ctx; ] -> Fmt.str {ocaml|let paf_ctx00 = Mimic.merge %s %s.ctx in Lwt.return paf_ctx00|ocaml} @@ -139,12 +125,11 @@ let mimic_paf_conf () = | _ -> assert false end -let mimic_paf_impl time pclock stackv4v6 paf mimic_tcp = +let mimic_paf_impl time pclock stackv4v6 mimic_tcp = mimic_paf_conf () $ time $ pclock $ stackv4v6 - $ paf $ mimic_tcp (* --- end of copied code --- *) @@ -197,36 +182,33 @@ let email = Key.(create "email" Arg.(opt (some string) None doc)) let packages = [ - package ~min:"2.6.0" "irmin"; - package ~min:"2.6.0" "irmin-mirage"; - package ~min:"2.6.0" "irmin-mirage-git"; - package "cohttp-mirage"; + package ~min:"3.5.0" "git-paf"; + package ~min:"3.5.0" "git"; + package ~min:"3.5.0" "git-mirage"; + package ~min:"2.8.0" ~max:"3.0.0" "irmin-mirage-git"; package "tls-mirage"; package "magic-mime"; package "logs"; package "awa"; package "awa-mirage"; - package ~min:"3.4.0" "git-mirage"; - package ~min:"0.2.5" "letsencrypt"; + package ~min:"3.6.0" ~max:"3.7.0" "git-mirage"; + package ~min:"0.3.0" "letsencrypt"; + package "paf" ~min:"0.0.6" ~sublibs:[ "mirage" ]; + package "paf-le"; ] let stack = generic_stackv4v6 default_network -let mimic_impl ~kind ~seed ~authenticator stackv4v6 random mclock pclock time paf = +let mimic_impl ~kind ~seed ~authenticator stackv4v6 random mclock pclock time = let mtcp = mimic_tcp_impl stackv4v6 in - let mdns = mimic_dns_impl random mclock time stackv4v6 mtcp in + let mdns = mimic_dns_impl random mclock pclock time stackv4v6 mtcp in let mssh = mimic_ssh_impl ~kind ~seed ~auth:authenticator stackv4v6 mtcp mclock in - let mpaf = mimic_paf_impl time pclock stackv4v6 paf mtcp in + let mpaf = mimic_paf_impl time pclock stackv4v6 mtcp in merge mpaf (merge mssh mdns) let mimic_impl = mimic_impl ~kind:`Rsa ~seed:ssh_seed ~authenticator:ssh_authenticator stack default_random default_monotonic_clock default_posix_clock default_time - (paf_impl default_time stack) - -let conduit_ = conduit_direct ~tls:true stack -let http_srv = cohttp_server conduit_ -let http_cli = cohttp_client (resolver_dns stack) conduit_ let () = let keys = Key.([ @@ -242,9 +224,11 @@ let () = ~keys ~packages "Unikernel.Main" - (mimic @-> http_client @-> http @-> pclock @-> time @-> job) + (mimic @-> random @-> mclock @-> pclock @-> time @-> stackv4v6 @-> job) $ mimic_impl - $ http_cli $ http_srv + $ default_random + $ default_monotonic_clock $ default_posix_clock $ default_time + $ stack ] diff --git a/unikernel.ml b/unikernel.ml index ab6f792..cc6e274 100644 --- a/unikernel.ml +++ b/unikernel.ml @@ -2,7 +2,18 @@ open Lwt.Infix let argument_error = 64 -module Main (_ : sig end) (Http_client: Cohttp_lwt.S.Client) (Http: Cohttp_mirage.Server.S) (C: Mirage_clock.PCLOCK) (Time: Mirage_time.S) = struct +module Main + (_ : sig end) + (Random: Mirage_random.S) + (M: Mirage_clock.MCLOCK) + (P: Mirage_clock.PCLOCK) + (Time: Mirage_time.S) + (Stack: Mirage_stack.V4V6) = struct + + module Nss = Ca_certs_nss.Make(P) + module Paf = Paf_mirage.Make(Time)(Stack) + module LE = LE.Make(Time)(Stack) + module DNS = Dns_client_mirage.Make(Random)(Time)(M)(P)(Stack) module Store = Irmin_mirage_git.Mem.KV(Irmin.Contents.String) module Sync = Irmin.Sync(Store) @@ -29,7 +40,7 @@ module Main (_ : sig end) (Http_client: Cohttp_lwt.S.Client) (Http: Cohttp_mirag let info = Store.Commit.info head in let ptime = match Ptime.of_float_s (Int64.to_float (Irmin.Info.date info)) with - | None -> Ptime.v (C.now_d_ps ()) + | None -> Ptime.v (P.now_d_ps ()) | Some d -> d in ptime_to_http_date ptime @@ -39,10 +50,9 @@ module Main (_ : sig end) (Http_client: Cohttp_lwt.S.Client) (Http: Cohttp_mirag last := (last_commit_date, last_commit_hash) let not_modified request = - let hdr = request.Cohttp.Request.headers in - match Cohttp.Header.get hdr "if-modified-since" with + match Httpaf.Headers.get request.Httpaf.Request.headers "if-modified-since" with | Some ts -> String.equal ts (fst !last) - | None -> match Cohttp.Header.get hdr "if-none-match" with + | None -> match Httpaf.Headers.get request.Httpaf.Request.headers "if-none-match" with | Some etags -> List.mem (snd !last) (Astring.String.cuts ~sep:"," etags) | None -> false @@ -73,181 +83,153 @@ module Main (_ : sig end) (Http_client: Cohttp_lwt.S.Client) (Http: Cohttp_mirag Sync.pull ~depth:1 store upstream `Set >>= fun r -> Last_modified.retrieve_last_commit store >|= fun () -> match r with - | Ok (`Head _ as s) -> Ok (Fmt.strf "pulled %a" Sync.pp_status s) + | Ok (`Head _ as s) -> Ok (Fmt.str "pulled %a" Sync.pp_status s) | Ok `Empty -> Error (`Msg "pulled empty repository") | Error (`Msg e) -> Error (`Msg ("pull error " ^ e)) | Error (`Conflict msg) -> Error (`Msg ("pull conflict " ^ msg)) end + let respond_with_empty reqd resp = + let hdr = Httpaf.Headers.add_unless_exists resp.Httpaf.Response.headers + "connection" "close" in + let resp = { resp with Httpaf.Response.headers= hdr } in + Httpaf.Reqd.respond_with_string reqd resp "" + module Dispatch = struct - let dispatch store hookf hook_url request _body = - let p = Uri.path (Cohttp.Request.uri request) in - let path = if String.equal p "/" then "index.html" else p in + let dispatch store hookf hook_url _conn reqd = + let request = Httpaf.Reqd.request reqd in + let path = request.Httpaf.Request.target in + let path = if String.equal path "/" then "index.html" else path in Logs.info (fun f -> f "requested %s" path); match Astring.String.cuts ~sep:"/" ~empty:false path with | [ h ] when String.equal hook_url h -> begin - hookf () >>= function - | Ok data -> Http.respond ~status:`OK ~body:(`String data) () + Lwt.async @@ fun () -> hookf () >>= function + | Ok data -> + let headers = Httpaf.Headers.of_list + [ "content-length", string_of_int (String.length data) ] in + let resp = Httpaf.Response.create ~headers `OK in + Httpaf.Reqd.respond_with_string reqd resp data ; + Lwt.return_unit | Error (`Msg msg) -> - Http.respond ~status:`Internal_server_error ~body:(`String msg) () + let headers = Httpaf.Headers.of_list + [ "content-length", string_of_int (String.length msg) ] in + let resp = Httpaf.Response.create ~headers `Internal_server_error in + Httpaf.Reqd.respond_with_string reqd resp msg ; + Lwt.return_unit end | path_list -> if Last_modified.not_modified request then - Http.respond ~status:`Not_modified ~body:`Empty () + let resp = Httpaf.Response.create `Not_modified in + respond_with_empty reqd resp else - Store.find store path_list >>= function + Lwt.async @@ fun () -> Store.find store (Store.Key.v path_list) >>= function | Some data -> - let mime_type = Magic_mime.lookup path in + let mime_type = Magic_mime.lookup path in (* TODO(dinosaure): replace by conan. *) let headers = [ "content-type", mime_type ; "etag", Last_modified.etag () ; "last-modified", Last_modified.last_modified () ; + "content-length", string_of_int (String.length data) ; ] in - let headers = Cohttp.Header.of_list headers in - Http.respond ~headers ~status:`OK ~body:(`String data) () + let headers = Httpaf.Headers.of_list headers in + let resp = Httpaf.Response.create ~headers `OK in + Httpaf.Reqd.respond_with_string reqd resp data ; + Lwt.return_unit | None -> let data = "Resource not found " ^ path in - Http.respond ~status:`Not_found ~body:(`String data) () - - let redirect port request _body = - let uri = Cohttp.Request.uri request in - let new_uri = Uri.with_scheme uri (Some "https") in + let headers = Httpaf.Headers.of_list + [ "content-length", string_of_int (String.length data) ] in + let resp = Httpaf.Response.create ~headers `Not_found in + Httpaf.Reqd.respond_with_string reqd resp data ; + Lwt.return_unit + + let redirect port _ reqd = + let request = Httpaf.Reqd.request reqd in let port = if port = 443 then None else Some port in - let new_uri = Uri.with_port new_uri port in + let path = request.Httpaf.Request.target in + let new_uri = Uri.make ~scheme:"https" ?host:(Key_gen.hostname ()) ?port ~path () in + (* TODO(dinosaure): check it. *) Logs.info (fun f -> f "[%s] -> [%s]" - (Uri.to_string uri) (Uri.to_string new_uri)); + path (Uri.to_string new_uri)); let headers = - Cohttp.Header.init_with "location" (Uri.to_string new_uri) - in - Http.respond ~headers ~status:`Moved_permanently ~body:`Empty () + Httpaf.Headers.of_list + [ "location", (Uri.to_string new_uri) ] in + let resp = Httpaf.Response.create ~headers `Moved_permanently in + respond_with_empty reqd resp end - module LE = struct - module Acme = Letsencrypt.Client.Make(Http_client) - - let gen_rsa ?seed () = - let g = match seed with - | None -> None - | Some seed -> - let seed = Cstruct.of_string seed in - Some (Mirage_crypto_rng.(create ~seed (module Fortuna))) - in - Mirage_crypto_pk.Rsa.generate ?g ~bits:4096 () + let ignore_error _ ?request:_ _ _ = () + let ( >>? ) = Lwt_result.bind - let csr key host = - match host with - | None -> - Logs.err (fun m -> m "no hostname provided"); - exit argument_error - | Some host -> - match Domain_name.of_string host with - | Error `Msg err -> - Logs.err (fun m -> m "invalid hostname provided %s" err); - exit argument_error - | Ok _ -> - let cn = - X509.[Distinguished_name.(Relative_distinguished_name.singleton (CN host))] - in - X509.Signing_request.create cn key - - let prefix = ".well-known", "acme-challenge" - let tokens = Hashtbl.create 1 - - let solver _host ~prefix:_ ~token ~content = - Hashtbl.replace tokens token content; - Lwt.return (Ok ()) - - let dispatch request _body = - let path = Uri.path (Cohttp.Request.uri request) in - Logs.info (fun m -> m "let's encrypt dispatcher %s" path); - match Astring.String.cuts ~sep:"/" ~empty:false path with - | [ p1; p2; token ] when - String.equal p1 (fst prefix) && String.equal p2 (snd prefix) -> - begin - match Hashtbl.find_opt tokens token with - | Some data -> - let headers = - Cohttp.Header.init_with "content-type" "application/octet-stream" - in - Http.respond ~headers ~status:`OK ~body:(`String data) () - | None -> Http.respond ~status:`Not_found ~body:`Empty () - end - | _ -> Http.respond ~status:`Not_found ~body:`Empty () - - let provision_certificate ctx = - let open Lwt_result.Infix in - let endpoint = - if Key_gen.production () then - Letsencrypt.letsencrypt_production_url - else - Letsencrypt.letsencrypt_staging_url - and email = Key_gen.email () - and seed = Key_gen.account_seed () - in - let priv = `RSA (gen_rsa ?seed:(Key_gen.cert_seed ()) ()) in - match csr priv (Key_gen.hostname ()) with - | Error (`Msg err) -> - Logs.err (fun m -> m "couldn't create signing request %s" err); - exit argument_error - | Ok csr -> - Acme.initialise ~ctx ~endpoint ?email (gen_rsa ?seed ()) >>= fun le -> - let sleep sec = Time.sleep_ns (Duration.of_sec sec) in - let solver = Letsencrypt.Client.http_solver solver in - Acme.sign_certificate ~ctx solver le sleep csr >|= fun certs -> - `Single (certs, priv) - end - - let serve cb = - let callback _ request body = cb request body - and conn_closed _ = () - in - Http.make ~conn_closed ~callback () + let request_handler store upstream : _ -> Httpaf.Server_connection.request_handler = + let hook_url = Key_gen.hook () in + if Astring.String.is_infix ~affix:"/" hook_url then begin + Logs.err (fun m -> m "hook url contains /, which is not allowed"); + exit argument_error + end else + let hookf () = Remote.pull store upstream in + Dispatch.dispatch store hookf hook_url - let start ctx http_client http () () = - Remote.connect ctx >>= fun (store, upstream) -> + let start git_ctx () () () () stackv4v6 = + Remote.connect git_ctx >>= fun (store, upstream) -> Lwt.map (function Ok () -> Lwt.return_unit | Error (`Msg msg) -> Lwt.fail_with msg) - (let open Lwt_result.Infix in - Remote.pull store upstream >>= fun data -> + (Remote.pull store upstream >>? fun data -> Logs.info (fun m -> m "store: %s" data); - let http_port = Key_gen.port () in - let tcp = `TCP http_port in - let server = - let hook_url = Key_gen.hook () in - if Astring.String.is_infix ~affix:"/" hook_url then begin - Logs.err (fun m -> m "hook url contains /, which is not allowed"); - exit argument_error - end else - let hookf () = Remote.pull store upstream in - serve (Dispatch.dispatch store hookf hook_url) - in if Key_gen.tls () then begin let rec provision () = + Paf.init ~port:80 stackv4v6 >>= fun t -> + let service = Paf.http_service + ~error_handler:ignore_error + LE.request_handler in + let stop = Lwt_switch.create () in + let `Initialized th0 = Paf.serve ~stop service t in Logs.info (fun m -> m "listening on 80/HTTP (let's encrypt provisioning)"); - (* this should be cancelled once certificates are retrieved *) - Lwt.async (fun () -> http (`TCP 80) (serve LE.dispatch)); - LE.provision_certificate http_client >>= fun certificates -> - let tls_cfg = Tls.Config.server ~certificates () in - let https_port = 443 in - let tls = `TLS (tls_cfg, `TCP https_port) in - let https = - Logs.info (fun f -> f "listening on %d/HTTPS" https_port); - http tls server - and http = - Logs.info (fun f -> f "listening on %d/HTTP, redirecting to %d/HTTPS" - http_port https_port); - let redirect = serve (Dispatch.redirect https_port) in - http tcp redirect - in - let expire = Time.sleep_ns (Duration.of_day 80) in - Lwt_result.ok (Lwt.pick [ https; http; expire ]) >>= fun () -> - provision () + let th1 = + LE.provision_certificate + ~production:(Key_gen.production ()) + { LE.certificate_seed = Key_gen.cert_seed () + ; LE.certificate_key_type = `ED25519 + ; LE.certificate_key_bits = Some 4096 + ; LE.email = Option.bind (Key_gen.email ()) (fun e -> Emile.of_string e |> Result.to_option) + ; LE.account_seed = Key_gen.account_seed () + ; LE.account_key_type = `ED25519 + ; LE.account_key_bits = Some 4096 + ; LE.hostname = Key_gen.hostname () |> Option.get |> Domain_name.of_string_exn |> Domain_name.host_exn } + (LE.ctx + ~gethostbyname:(fun dns domain_name -> DNS.gethostbyname dns domain_name >>? fun ipv4 -> Lwt.return_ok (Ipaddr.V4 ipv4)) + ~authenticator:(Result.get_ok (Nss.authenticator ())) + (DNS.create stackv4v6) stackv4v6) + >>? fun certificates -> + Lwt_switch.turn_off stop >>= fun () -> Lwt.return_ok certificates in + Lwt.both th0 th1 >>= function + | ((), (Error _ as err)) -> Lwt.return err + | ((), Ok certificates) -> + Logs.debug (fun m -> m "Got certificates from let's encrypt.") ; + let tls = Tls.Config.server ~certificates () in + Paf.init ~port:(Key_gen.port ()) stackv4v6 >>= fun t -> + let service = Paf.https_service ~tls + ~error_handler:ignore_error + (request_handler store upstream) in + let stop = Lwt_switch.create () in + let `Initialized th = Paf.serve ~stop service t in + Logs.info (fun m -> + m "listening on %d/HTTPS" (Key_gen.port ())); + Lwt.both th + (Time.sleep_ns (Duration.of_day 80) >>= fun () -> Lwt_switch.turn_off stop) + >>= fun ((), ()) -> + provision () in provision () end else begin - Logs.info (fun f -> f "listening on %d/HTTP" http_port); - Lwt_result.ok (http tcp server) + Paf.init ~port:(Key_gen.port ()) stackv4v6 >>= fun t -> + let service = Paf.http_service + ~error_handler:ignore_error + (request_handler store upstream) in + let `Initialized th = Paf.serve service t in + Logs.info (fun f -> f "listening on %d/HTTP" (Key_gen.port ())); + (th >|= fun v -> Ok v) end) end