diff --git a/README.md b/README.md index e41c6c7..1614a01 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Vif, a small framework for building a web server from an OCaml script +# νιϝ, a small framework for building a web server from an OCaml script + +(nu)(iota)(digamma) **disclaimer**: Please note that this is an experimental project. It's also an opportunity to build something that can be satisfying for web development. diff --git a/examples/09-jwt/cookie.txt b/examples/09-jwt/cookie.txt new file mode 100644 index 0000000..005fc86 --- /dev/null +++ b/examples/09-jwt/cookie.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / TRUE 0 __Host-vif-token AIT3qkbKq7NBg7ePl7Xbs9ikzUNdcxLuKYbinTM3KQdTVm2jAHY36a8c9mOFSf5opEudhbozpU2oXxt_bfCkR9YlqAlzjK8OYcLwgBVMLZaJBiontKM9BdS70yUDdPawY4h883n9P0A9lEUhMNMQr7-NVieuIgZTGbtTV-B7Xrcf9pipVYOIiyiPcILaoDwa-ycs-_b9yqlyzDYfMPOwsWphSPR0Ji9lVg diff --git a/examples/09-jwt/main.ml b/examples/09-jwt/main.ml index e09591a..3160fd1 100644 --- a/examples/09-jwt/main.ml +++ b/examples/09-jwt/main.ml @@ -14,11 +14,8 @@ type cfg = let jwt = Vif.Ms.make ~name:"jwt" @@ fun req target server { secret } -> Logs.debug (fun m -> m "Search vif-token cookie"); match Cookie.get server req ~name:"vif-token" with - | Error err -> - Logs.err (fun m -> m "jwt: %a" Cookie.pp_error err); - None + | Error err -> None | Ok token -> - Logs.debug (fun m -> m "Token found: %S" token); let ( let* ) = Option.bind in let* token = Result.to_option (Jwto.decode_and_verify secret token) in let* username = List.assoc_opt "username" (Jwto.get_payload token) in @@ -42,27 +39,39 @@ let credential = conv prj inj credential ;; +let form = + let open Vif.Multipart_form in + let fn username password = + { username; password } in + record fn + |+ field "username" string + |+ field "password" string + |> sealr +;; + let users = [ "dinosaure", "foo" ] ;; -let login req server { secret }= +let login req server { secret } { username; password } = + match List.assoc_opt username users with + | Some p' when password = p' -> + let token = Jwto.encode HS512 secret [ "username", username ] in + let token = Result.get_ok token in + let* () = Vif.Cookie.set ~name:"vif-token" server req token in + let field = "content-type" in + let* () = Response.add ~field "text/plain; charset=utf-8" in + let* () = Response.with_string req "Authenticated!\n" in + Response.respond `OK + | _ -> + let field = "content-type" in + let* () = Response.add ~field "text/plain; charset= utf-8" in + let* () = Response.with_string req "Bad credentials\n" in + Response.respond `Unauthorized + +let login_by_json req server cfg = match Vif.Request.of_json req with - | Ok { username; password } -> - begin match List.assoc_opt username users with - | Some p' when password = p' -> - let token = Jwto.encode HS512 secret [ "username", username ] in - let token = Result.get_ok token in - let* () = Vif.Cookie.set ~name:"vif-token" server req token in - let field = "content-type" in - let* () = Response.add ~field "text/plain; charset=utf-8" in - let* () = Response.with_string req "Authenticated!\n" in - Response.respond `OK - | _ -> - let field = "content-type" in - let* () = Response.add ~field "text/plain; charset= utf-8" in - let* () = Response.with_string req "Bad credentials\n" in - Response.respond `Unauthorized end + | Ok credential -> login req server cfg credential | Error _ -> let field = "content-type" in let* () = Response.add ~field "text/plain; charset=utf-8" in @@ -70,6 +79,16 @@ let login req server { secret }= Response.respond (`Code 422) ;; +let login_by_form req server cfg = + match Vif.Request.of_multipart_form req with + | Ok credential -> login req server cfg credential + | Error _ -> + let field = "content-type" in + let* () = Response.add ~field "text/plain; charset=utf-8" in + let* () = Response.with_string req "Invalid multipart-form\n" in + Response.respond (`Code 422) +;; + let default req server _cfg = match Request.get jwt req with | None -> @@ -88,8 +107,9 @@ let default req server _cfg = let routes = let open Vif.U in let open Vif.R in - let open Vif.Content_type in - [ post (json_encoding credential) (rel / "login" /?? nil) --> login + let open Vif.T in + [ post (m form) (rel / "login" /?? nil) --> login_by_form + ; post (json_encoding credential) (rel / "login" /?? nil) --> login_by_json ; get (rel /?? nil) --> default ] ;; diff --git a/examples/12-static/index.html b/examples/12-static/index.html index d8a2cd6..2850e0f 100644 --- a/examples/12-static/index.html +++ b/examples/12-static/index.html @@ -1 +1,8 @@ -

Hello from Vif!

+ + + Vif + + +

Hello from Vif!

+ + diff --git a/examples/12-static/main.ml b/examples/12-static/main.ml index c26c1bf..e01545d 100644 --- a/examples/12-static/main.ml +++ b/examples/12-static/main.ml @@ -2,6 +2,17 @@ open Vif ;; -let () = Miou_unix.run @@ fun () -> - Vif.run ~handlers:[ Handler.static ] [] () +let default req server _ = + Response.with_file ~compression:`DEFLATE req (Fpath.v "index.html") +;; + +let routes = + let open Vif.U in + let open Vif.R in + let open Vif.Content_type in + [ get (rel /?? nil) --> default ] +;; + +let () = Miou_unix.run @@ fun () -> + Vif.run ~handlers:[ Handler.static ] routes () ;; diff --git a/lib/bin.ml b/lib/bin.ml new file mode 100644 index 0000000..e69de29 diff --git a/lib/vif/dune b/lib/vif/dune index 743808d..a8e70f7 100644 --- a/lib/vif/dune +++ b/lib/vif/dune @@ -15,5 +15,6 @@ decompress.zl decompress.gz mirage-crypto-rng-miou-unix + multipart_form-miou httpcats tyre)) diff --git a/lib/vif/stream.ml b/lib/vif/stream.ml index 37e24dc..243e7b9 100644 --- a/lib/vif/stream.ml +++ b/lib/vif/stream.ml @@ -178,6 +178,17 @@ module Sink = struct let stop = Buffer.contents in Sink { init; push; full; stop } + let into_bstream bstream = + let open Multipart_form_miou in + let init () = bstream in + let push bstream str = + Bounded_stream.put bstream (Some str); + bstream + in + let full = Fun.const false in + let stop bstream = Bounded_stream.put bstream None in + Sink { init; push; full; stop } + let json () = let decoder = Jsonm.decoder `Manual in let rec error (`Error err) = diff --git a/lib/vif/stream.mli b/lib/vif/stream.mli index 74d3207..9a44b64 100644 --- a/lib/vif/stream.mli +++ b/lib/vif/stream.mli @@ -31,6 +31,7 @@ type ('a, 'r) sink = module Sink : sig val json : unit -> (string, (Json.t, [ `Msg of string ]) result) sink + val into_bstream : 'a Multipart_form_miou.Bounded_stream.t -> ('a, unit) sink val string : (string, string) sink end diff --git a/lib/vif/vif.ml b/lib/vif/vif.ml index c4f72f0..e9bd4b9 100644 --- a/lib/vif/vif.ml +++ b/lib/vif/vif.ml @@ -6,7 +6,7 @@ module U = Vif_u module R = struct include Vif_r - open Vif_content_type + open Vif_t type ('fu, 'return) t = | Handler : ('f, 'x) Vif_r.req * ('x, 'r) Vif_u.t -> ('f, 'r) t @@ -70,13 +70,51 @@ module Ms = struct end end -module Content_type = Vif_content_type +module T = Vif_t module Stream = Stream module Method = Vif_method module Status = Vif_status module Headers = Vif_headers -module Request = Vif_request -module Response = Vif_response + +module Response = struct + include Vif_response + + let mime_type path = + let default = "application/octet-stream" in + match Conan_unix.run_with_tree Conan_light.tree (Fpath.to_string path) with + | Ok m -> Option.value ~default (Conan.Metadata.mime m) + | Error _ -> default + | exception _ -> default + + let with_file ?mime ?compression:alg req path = + if + Sys.file_exists (Fpath.to_string path) = false + || Sys.is_directory (Fpath.to_string path) + then Fmt.invalid_arg "Response.with_file %a" Fpath.pp path; + if Vif_handler.cache req path then + let* () = with_string req "" in + respond `Not_modified + else + let mime = Option.value ~default:(mime_type path) mime in + let src = Stream.Source.file (Fpath.to_string path) in + let src = Stream.Stream.from src in + let field = "connection" in + let* () = + if Vif_request.version req = 1 then add ~field "close" else return () + in + let field = "content-type" in + let* () = add ~field mime in + let stat = Unix.stat (Fpath.to_string path) in + let field = "content-length" in + let* () = add ~field (string_of_int stat.Unix.st_size) in + let none = return false in + let* _ = Option.fold ~none ~some:(fun alg -> compression alg req) alg in + let field = "etag" in + let* () = add ~field (Vif_handler.sha256sum path) in + let* () = with_stream req src in + respond `OK +end + module Cookie = Vif_cookie module Handler = Vif_handler @@ -90,6 +128,11 @@ let return = Response.return let is_application_json { Multipart_form.Content_type.ty; subty; _ } = match (ty, subty) with `Application, `Iana_token "json" -> true | _ -> false +let is_multipart_form_data { Multipart_form.Content_type.ty; subty; _ } = + match (ty, subty) with + | `Multipart, `Iana_token "form-data" -> true + | _ -> false + let content_type req0 = let headers = Vif_request0.headers req0 in let c = Vif_headers.get headers "content-type" in @@ -97,53 +140,125 @@ let content_type req0 = let c = Option.to_result ~none:`Not_found c in Result.bind c Multipart_form.Content_type.of_string -[@@@warning "-8"] - let recognize_request ~env req0 = let extract : type c a. - Vif_method.t option - -> (c, a) Vif_content_type.t - -> (c, a) Vif_request.t option = + Vif_method.t option -> (c, a) Vif_t.t -> (c, a) Vif_request.t option = fun meth c -> - let meth' = Vif_request0.meth req0 in - match (meth, meth', c) with - | None, _, (Vif_content_type.Any as encoding) -> - Some (Vif_request.of_req0 ~encoding ~env req0) - | Some a, b, (Vif_content_type.Any as encoding) -> - if a = b then Some (Vif_request.of_req0 ~encoding ~env req0) else None - | None, _, (Null as encoding) -> - Some (Vif_request.of_req0 ~encoding ~env req0) - | Some a, b, (Null as encoding) -> - if a = b then Some (Vif_request.of_req0 ~encoding ~env req0) else None - | None, _, (Json_encoding _ as encoding) -> - let c = content_type req0 in - let application_json = Result.map is_application_json c in - let application_json = Result.value ~default:false application_json in - if application_json then Some (Vif_request.of_req0 ~encoding ~env req0) + let none = true in + let some = ( = ) (Vif_request0.meth req0) in + let meth_match = Option.fold ~none ~some meth in + Log.debug (fun m -> m "method matches? %b" meth_match); + match c with + | Vif_t.Any as encoding -> + if meth_match then Some (Vif_request.of_req0 ~encoding ~env req0) else None - | Some a, b, (Json_encoding _ as encoding) -> + | Null as encoding -> + if meth_match then Some (Vif_request.of_req0 ~encoding ~env req0) + else None + | Json_encoding _ as encoding -> let c = content_type req0 in - let application_json = Result.map is_application_json c in - let application_json = Result.value ~default:false application_json in - if application_json && a = b then + let type_match = Result.map is_application_json c in + let type_match = Result.value ~default:false type_match in + if type_match && meth_match then Some (Vif_request.of_req0 ~encoding ~env req0) else None - | None, _, (Json as encoding) -> + | Multipart_form_encoding _ as encoding -> let c = content_type req0 in - let application_json = Result.map is_application_json c in - let application_json = Result.value ~default:false application_json in - if application_json then Some (Vif_request.of_req0 ~encoding ~env req0) - else None - | Some a, b, (Json as encoding) -> - let c = content_type req0 in - let application_json = Result.map is_application_json c in - let application_json = Result.value ~default:false application_json in - if application_json && a = b then + let type_match = Result.map is_multipart_form_data c in + let type_match = Result.value ~default:false type_match in + if type_match && meth_match then Some (Vif_request.of_req0 ~encoding ~env req0) else None + | Json as encoding -> + let c = content_type req0 in + let type_match = Result.map is_application_json c in + let type_match = Result.value ~default:false type_match in + if type_match && meth_match then + Some (Vif_request.of_req0 ~encoding ~env req0) + else None + | Multipart_form -> assert false (* TODO *) in { Vif_r.extract } +module Multipart_form = struct + include Vif_multipart_form + + let parse req = + let open Multipart_form_miou in + let hdrs = Vif_request.headers req in + let ct = + match Vif_headers.get hdrs "content-type" with + | None -> Fmt.invalid_arg "Content-type field missing" + | Some str -> + let ct = Multipart_form.Content_type.of_string (str ^ "\r\n") in + Result.get_ok ct + in + let t = Bounded_stream.create 0x100 in + let prm0 = + Miou.async @@ fun () -> + let open Stream in + Stream.into (Sink.into_bstream t) (Vif_request.stream req) + in + let prm1 = Miou.async @@ fun () -> of_stream_to_list t ct in + Miou.await_exn prm0; + match Miou.await_exn prm1 with + | Ok (_tree, lst) -> + let fn (_id, hdrs) = + let hdrs = Multipart_form.Header.to_list hdrs in + let name = ref None in + let filename = ref None in + let size = ref None in + let mime = ref None in + let fn = function + | Multipart_form.Field.Field (_, Content_type, { ty; subty; _ }) -> + let open Multipart_form.Content_type in + let value = Fmt.str "%a/%a" Type.pp ty Subtype.pp subty in + mime := Some value; + None + | Field (_, Content_encoding, _) -> None + | Field (_, Content_disposition, t) -> + let open Multipart_form in + name := Content_disposition.name t; + filename := Content_disposition.filename t; + size := Content_disposition.size t; + None + | Field (fn, Field, unstrctrd) -> + let k = (fn :> string) in + let v = Unstrctrd.fold_fws unstrctrd in + let v = Unstrctrd.to_utf_8_string v in + Some (k, v) + in + let hdrs = List.filter_map fn hdrs in + let meta = + { name= !name; filename= !filename; size= !size; mime= !mime } + in + (meta, hdrs) + in + Ok (List.map (fun (k, v) -> (fn k, v)) lst) + | Error (`Msg msg) -> + Logs.err (fun m -> m "Invalid multipart/form-data: %s" msg); + Error `Invalid_multipart_form +end + +module Request = struct + include Vif_request + + let of_multipart_form : type a. + (Vif_t.multipart_form, a) Vif_request.t + -> (a, [> `Invalid_multipart_form | `Not_found of string ]) result = + function + | { encoding= Multipart_form_encoding r; _ } as req -> + let ( let* ) = Result.bind in + let* raw = Multipart_form.parse req in + begin + try Ok (Multipart_form.get_record r raw) + with Multipart_form.Field_not_found field -> + Error (`Not_found field) + end + | { encoding= Multipart_form; _ } -> assert false + | { encoding= Any; _ } -> assert false +end + type 'value daemon = { queue: 'value user's_function Queue.t ; mutex: Miou.Mutex.t @@ -192,7 +307,7 @@ let rec user's_functions daemon = in let fn (User's_task (req0, fn)) = let _prm = - Miou.call ~orphans:daemon.orphans @@ fun () -> + Miou.async ~orphans:daemon.orphans @@ fun () -> match Vif_response.(run req0 empty) (fn daemon.server daemon.user's_value) with @@ -212,6 +327,11 @@ let handler _cfg ~default ~middlewares routes daemon = let request = recognize_request ~env req0 in let target = Vif_request0.target req0 in let fn = R.dispatch ~default routes ~request ~target in + (* NOTE(dinosaure): the management of the http request must finish and above + all **not** block. Otherwise, the entire domain is blocked. Thus, the + management of a new request transfers the user task (which can block) to + our "daemon" instantiated in our current domain which runs cooperatively. + *) begin Miou.Mutex.protect daemon.mutex @@ fun () -> Queue.push (User's_task (req0, fn)) daemon.queue; @@ -223,20 +343,41 @@ type config = Vif_config.config let () = Sys.set_signal Sys.sigpipe Sys.Signal_ignore let config = Vif_config.config -let handle stop cfg fn = +let process stop cfg server user's_value fn = Logs.debug (fun m -> m "new HTTP server on [%d]" (Stdlib.Domain.self () :> int)); + let daemon = + { + queue= Queue.create () + ; mutex= Miou.Mutex.create () + ; orphans= Miou.orphans () + ; condition= Miou.Condition.create () + ; user's_value + ; server + } + in + let fn = fn daemon in + let user's_tasks = Miou.async @@ fun () -> user's_functions daemon in + let parallel = false in + (* NOTE(dinosaure): The user task **must** be executed cooperatively (instead + of in parallel) with the task managing the new http connection. [httpcats] + is therefore instructed to launch the task managing the http connection on + the same domain as the [process] domain. *) match (cfg.Vif_config.http, cfg.Vif_config.tls) with | config, Some tls -> - Httpcats.Server.with_tls ?stop ?config ~backlog:cfg.backlog tls - ~handler:fn cfg.sockaddr + Httpcats.Server.with_tls ~parallel ?stop ?config ~backlog:cfg.backlog tls + ~handler:fn cfg.sockaddr; + Miou.cancel user's_tasks | Some (`H2 _), None -> + Miou.cancel user's_tasks; failwith "Impossible to launch an h2 server without TLS." | Some (`Both (config, _) | `HTTP_1_1 config), None -> - Httpcats.Server.clear ?stop ~config ~handler:fn cfg.sockaddr + Httpcats.Server.clear ~parallel ?stop ~config ~handler:fn cfg.sockaddr; + Miou.cancel user's_tasks | None, None -> Log.debug (fun m -> m "Start a non-tweaked HTTP/1.1 server"); - Httpcats.Server.clear ?stop ~handler:fn cfg.sockaddr + Httpcats.Server.clear ~parallel ?stop ~handler:fn cfg.sockaddr; + Miou.cancel user's_tasks let store_pid = function | None -> () @@ -283,7 +424,7 @@ let run ?(cfg = Vif_options.config_from_globals ()) ?(devices = Ds.[]) let finally () = Mirage_crypto_rng_miou_unix.kill rng in Fun.protect ~finally @@ fun () -> let interactive = !Sys.interactive in - let domains = Miou.Domain.available () in + let domains = cfg.domains in store_pid cfg.pid; let stop = match interactive with @@ -302,31 +443,21 @@ let run ?(cfg = Vif_options.config_from_globals ()) ?(devices = Ds.[]) let devices = Ds.run Vif_d.Hmap.empty devices user's_value in Logs.debug (fun m -> m "devices launched"); let server = { Vif_s.devices; cookie_key= cfg.Vif_config.cookie_key } in - let daemon = - { - queue= Queue.create () - ; mutex= Miou.Mutex.create () - ; orphans= Miou.orphans () - ; condition= Miou.Condition.create () - ; user's_value - ; server - } - in let default = default_from_handlers handlers in - let user's_tasks = Miou.call @@ fun () -> user's_functions daemon in - let fn0 = handler cfg ~default ~middlewares routes daemon in - let prm0 = Miou.async @@ fun () -> handle stop cfg fn0 in + let fn0 = handler cfg ~default ~middlewares routes in + let prm0 = Miou.async @@ fun () -> process stop cfg server user's_value fn0 in let tasks = - let fn _ = handler cfg ~default ~middlewares routes daemon in + let fn _ = handler cfg ~default ~middlewares routes in List.init domains fn in let tasks = - if domains > 0 then Miou.parallel (handle stop cfg) tasks else [] + if domains > 0 then + Miou.parallel (process stop cfg server user's_value) tasks + else [] in Miou.await_exn prm0; List.iter (function Ok () -> () | Error exn -> raise exn) tasks; Ds.finally (Vif_d.Devices devices); - Miou.cancel user's_tasks; Log.debug (fun m -> m "Vif (and devices) terminated") let setup_config = Vif_options.setup_config diff --git a/lib/vif/vif.mli b/lib/vif/vif.mli index 1515b0c..dae6dbd 100644 --- a/lib/vif/vif.mli +++ b/lib/vif/vif.mli @@ -51,14 +51,33 @@ module Method : sig | `Other of string ] end -module Content_type : sig +module Multipart_form : sig + type 'a t + type 'a atom + + val string : string atom + + type ('a, 'b, 'c) orecord + + val record : 'b -> ('a, 'b, 'b) orecord + + type 'a field + + val field : string -> 'a atom -> 'a field + val ( |+ ) : ('a, 'b, 'c -> 'd) orecord -> 'c field -> ('a, 'b, 'd) orecord + val sealr : ('a, 'b, 'a) orecord -> 'a t +end + +module T : sig type null type json + type multipart_form type ('c, 'a) t val null : (null, unit) t val json : (json, Json.t) t val json_encoding : 'a Json_encoding.encoding -> (json, 'a) t + val m : 'a Multipart_form.t -> (multipart_form, 'a) t val any : ('c, string) t end @@ -73,7 +92,12 @@ module Request : sig val meth : ('c, 'a) t -> Method.t val version : ('c, 'a) t -> int val headers : ('c, 'a) t -> Headers.t - val of_json : (Content_type.json, 'a) t -> ('a, [ `Msg of string ]) result + val of_json : (T.json, 'a) t -> ('a, [ `Msg of string ]) result + + val of_multipart_form : + (T.multipart_form, 'a) t + -> ('a, [ `Not_found of string | `Invalid_multipart_form ]) result + val stream : ('c, 'a) t -> string Stream.stream val get : ('cfg, 'v) M.t -> ('c, 'a) t -> 'v option @@ -87,21 +111,10 @@ end module R : sig type 'r route type ('fu, 'return) t - type request - - val get : ('x, 'r) U.t -> ((Content_type.null, unit) Request.t -> 'x, 'r) t - - val post : - ('c, 'a) Content_type.t -> ('x, 'r) U.t -> (('c, 'a) Request.t -> 'x, 'r) t + val get : ('x, 'r) U.t -> ((T.null, unit) Request.t -> 'x, 'r) t + val post : ('c, 'a) T.t -> ('x, 'r) U.t -> (('c, 'a) Request.t -> 'x, 'r) t val ( --> ) : ('f, 'r) t -> 'f -> 'r route - - val dispatch : - default:(('c, string) Request.t -> string -> 'r) - -> 'r route list - -> request:request - -> target:string - -> 'r end module C : sig @@ -137,11 +150,11 @@ module C : sig let readme = let open U in host "raw.githubusercontent.com" - /% Tyre.string - /% Tyre.string + /% string + /% string / "refs" / "heads" - /% Tyre.string + /% string / "README.md" /?? nil @@ -151,6 +164,19 @@ module C : sig end module D : sig + (** {3 Devices.} + + A device is a global instance on the http server with which a "finaliser" + is associated. A device is available from all requests from a {!type:S.t} + value. The same device instance is available from all domains — + interactions with a device must therefore be {i domain-safe}. + + A device can be created from several values as well as from other devices. + Finally, a device is constructed from an end-user value specified by + {!val:Vif.run}. The idea is to allow the user to construct a value (from, + for example, command line parameters) corresponding to a configuration and + to construct these devices from this value. *) + type ('value, 'a) arg type ('value, 'a) device @@ -245,6 +271,14 @@ module Status : sig end module Response : sig + (** {3 Response.} + + A response is a construction (monad) whose initial state is {i empty} + ({!type:e}) and must end in the state {i sent} ({!type:s}). Throughout + this construction, the user can {!val:add}/{!val:rem}/{!val:set} + information in the {i header}. Finally, the user must respond with content + (via {!val:with_string}/{!val:with_stream}) and a status code. *) + type ('p, 'q, 'a) t type e type f @@ -259,6 +293,13 @@ module Response : sig val with_string : ?compression:[< `DEFLATE ] -> ('c, 'a) Request.t -> string -> (e, f, unit) t + val with_file : + ?mime:string + -> ?compression:[< `DEFLATE ] + -> ('c, 'a) Request.t + -> Fpath.t + -> (e, s, unit) t + val respond : Status.t -> (f, s, unit) t (** Headers manipulation. *) @@ -283,17 +324,16 @@ module Cookie : sig -> unit -> config + type error = [ `Invalid_encrypted_cookie | `Msg of string | `Not_found ] + val get : ?encrypted:bool -> name:string -> S.t -> Request.request - -> ( string - , [> `Invalid_encrypted_cookie | `Msg of string | `Not_found ] ) - result + -> (string, [> error ]) result - val pp_error : - [ `Invalid_encrypted_cookie | `Msg of string | `Not_found ] Fmt.t + val pp_error : error Fmt.t val set : ?encrypt:bool @@ -330,7 +370,8 @@ val ( let* ) : val return : 'a -> ('p, 'p, 'a) Response.t val config : - ?cookie_key:Mirage_crypto.AES.GCM.key + ?domains:int + -> ?cookie_key:Mirage_crypto.AES.GCM.key -> ?pid:Fpath.t -> ?http: [ `H1 of H1.Config.t diff --git a/lib/vif/vif_config.ml b/lib/vif/vif_config.ml index 074b6e1..cbe8e9e 100644 --- a/lib/vif/vif_config.ml +++ b/lib/vif/vif_config.ml @@ -9,6 +9,7 @@ type config = { ; sockaddr: Unix.sockaddr ; pid: Fpath.t option ; cookie_key: Mirage_crypto.AES.GCM.key + ; domains: int } let really_bad_secret = @@ -17,8 +18,10 @@ let really_bad_secret = let hash = SHA256.to_raw_string hash in Mirage_crypto.AES.GCM.of_secret hash -let config ?(cookie_key = really_bad_secret) ?pid ?http ?tls ?(backlog = 64) - sockaddr = +let default_domains = Int.min (Stdlib.Domain.recommended_domain_count ()) 4 + +let config ?(domains = default_domains) ?(cookie_key = really_bad_secret) ?pid + ?http ?tls ?(backlog = 64) sockaddr = let http = match http with | Some (`H1 cfg) -> Some (`HTTP_1_1 cfg) @@ -26,4 +29,4 @@ let config ?(cookie_key = really_bad_secret) ?pid ?http ?tls ?(backlog = 64) | Some (`Both (h1, h2)) -> Some (`Both (h1, h2)) | None -> None in - { http; tls; backlog; sockaddr; pid; cookie_key } + { http; tls; backlog; sockaddr; pid; cookie_key; domains } diff --git a/lib/vif/vif_content_type.ml b/lib/vif/vif_content_type.ml deleted file mode 100644 index e92cc3f..0000000 --- a/lib/vif/vif_content_type.ml +++ /dev/null @@ -1,55 +0,0 @@ -type null = Null -type json = Json -type multipart_form = Multipart_form -type stream = string Stream.source Multipart_form.elt Stream.source - -type ('c, 'a) t = - | Null : (null, unit) t - | Json_encoding : 'a Json_encoding.encoding -> (json, 'a) t - | Multipart_form_encoding : 'a Multipart_form.t -> (multipart_form, 'a) t - | Multipart_form : (multipart_form, stream) t - | Json : (json, Json.t) t - | Any : ('c, string) t - -let null = Null -let json_encoding e = Json_encoding e -let json = Json -let any = Any - -module Witness = struct - type (_, _) eq = Refl : ('a, 'a) eq - type _ equality = .. - - module type Inst = sig - type t - type _ equality += Eq : t equality - end - - type 'a t = (module Inst with type t = 'a) - - let make : type a. unit -> a t = - fun () -> - let module Inst = struct - type t = a - type _ equality += Eq : t equality - end in - (module Inst) - - let _eq : type a b. a t -> b t -> (a, b) eq option = - fun (module A) (module B) -> match A.Eq with B.Eq -> Some Refl | _ -> None -end - -module Multipart = struct - type 'a t = { rwit: 'a Witness.t; rfields: 'a fields_and_constr } - - and 'a fields_and_constr = - | Fields : ('a, 'b) fields * 'b -> 'a fields_and_constr - - and ('a, 'b) fields = - | F0 : ('a, 'a) fields - | F1 : ('a, 'b) field * ('a, 'c) fields -> ('a, 'b -> 'c) fields - - and ('a, 'b) field = { name: string; ftype: 'b ty; fget: 'a -> 'b } - and 'a ty = Primary : 'a primary -> 'a ty | Record : 'a t -> 'a ty - and 'a primary = String : string primary -end diff --git a/lib/vif/vif_cookie.ml b/lib/vif/vif_cookie.ml index 62d7607..bac0866 100644 --- a/lib/vif/vif_cookie.ml +++ b/lib/vif/vif_cookie.ml @@ -32,6 +32,8 @@ let all_cookies hdrs = let guard error fn = if fn () then Ok () else Error error let err_cookie = `Invalid_encrypted_cookie +type error = [ `Invalid_encrypted_cookie | `Msg of string | `Not_found ] + let pp_error ppf = function | `Invalid_encrypted_cookie -> Fmt.string ppf "Invalid encrypted cookie" | `Not_found -> Fmt.string ppf "Cookie not found" diff --git a/lib/vif/vif_multipart_form.ml b/lib/vif/vif_multipart_form.ml new file mode 100644 index 0000000..daa78c9 --- /dev/null +++ b/lib/vif/vif_multipart_form.ml @@ -0,0 +1,143 @@ +module Witness = struct + type (_, _) eq = Refl : ('a, 'a) eq + type _ equality = .. + + module type Inst = sig + type t + type _ equality += Eq : t equality + end + + type 'a t = (module Inst with type t = 'a) + + let make : type a. unit -> a t = + fun () -> + let module Inst = struct + type t = a + type _ equality += Eq : t equality + end in + (module Inst) + + let _eq : type a b. a t -> b t -> (a, b) eq option = + fun (module A) (module B) -> match A.Eq with B.Eq -> Some Refl | _ -> None +end + +type 'a t = { rwit: 'a Witness.t; rfields: 'a fields_and_constr } + +and 'a fields_and_constr = + | Fields : ('a, 'b) fields * 'b -> 'a fields_and_constr + +and ('a, 'b) fields = + | F0 : ('a, 'a) fields + | F1 : 'b field * ('a, 'c) fields -> ('a, 'b -> 'c) fields + +and 'a field = { fname: string; ftype: 'a atom } +and 'a atom = Primary : 'a primary -> 'a atom | Record : 'a t -> 'a atom +and 'a primary = String : string primary + +type meta = { + name: string option + ; filename: string option + ; size: int option + ; mime: string option +} + +let pp_meta ppf t = + match (t.name, t.filename) with + | Some name, _ -> Fmt.string ppf name + | _, Some filename -> Fmt.string ppf filename + | _ -> Fmt.pf ppf "" + +type raw = ((meta * Vif_headers.t) * string) list + +module Fields_folder (Acc : sig + type ('a, 'b) t +end) = +struct + type 'a t = { + nil: ('a, 'a) Acc.t + ; cons: 'b 'c. 'b field -> ('a, 'c) Acc.t -> ('a, 'b -> 'c) Acc.t + } + + let rec fold : type a c. a t -> (a, c) fields -> (a, c) Acc.t = + fun folder -> function + | F0 -> folder.nil + | F1 (f, fs) -> folder.cons f (fold folder fs) +end + +module Record_get = Fields_folder (struct + type ('a, 'b) t = raw -> 'b -> 'a +end) + +exception Field_not_found of string + +let find_by_name name raw = + let fn ((meta, _), _) = + match meta.name with Some name' -> String.equal name name' | None -> false + in + let _, value = + try List.find fn raw with Not_found -> raise (Field_not_found name) + in + value + +let rec get_value : type a. a atom -> string -> raw -> a = function + | Primary String -> find_by_name + | Record r -> fun _ raw -> get_record r raw + +and get_record : type a. a t -> raw -> a = + fun { rfields= Fields (fs, constr); _ } -> + let nil _raw fn = fn in + let cons { fname; ftype } k = + let get = get_value ftype fname in + fun raw constr -> + let x = get raw in + let constr = constr x in + k raw constr + in + let fn = Record_get.fold { nil; cons } fs in + fun raw -> fn raw constr + +type ('a, 'b, 'c) orecord = ('a, 'c) fields -> 'b * ('a, 'b) fields +type 'a a_field = Field : 'x field -> 'a a_field + +let field fname ftype = { fname; ftype } +let record : 'b -> ('a, 'b, 'b) orecord = fun c fs -> (c, fs) + +module SSet = Set.Make (String) + +let check_unique fn = + let rec go s = function + | [] -> () + | x :: xs -> ( + match SSet.find_opt x s with + | None -> go (SSet.add x s) xs + | Some _ -> fn x) + in + go SSet.empty + +let check_uniq_field_names rfields = + let names = List.map (fun (Field { fname; _ }) -> fname) rfields in + let failure fname = + Fmt.invalid_arg "The name %s was used for two or more parts." fname + in + check_unique failure names + +let fields r = + let rec go : type a b. (a, b) fields -> a a_field list = function + | F0 -> [] + | F1 (h, t) -> Field h :: go t + in + match r.rfields with Fields (f, _) -> go f + +let app : type a b c d. (a, b, c -> d) orecord -> c field -> (a, b, d) orecord = + fun r f fs -> r (F1 (f, fs)) + +let sealr : type a b. (a, b, a) orecord -> a t = + fun r -> + let c, fs = r F0 in + let rwit = Witness.make () in + let sealed = { rwit; rfields= Fields (fs, c) } in + check_uniq_field_names (fields sealed); + sealed + +let ( |+ ) = app +let string = Primary String diff --git a/lib/vif/vif_options.ml b/lib/vif/vif_options.ml index 0eaa7c2..41c528f 100644 --- a/lib/vif/vif_options.ml +++ b/lib/vif/vif_options.ml @@ -3,16 +3,18 @@ let port = ref 8080 let inet_addr = ref Unix.inet_addr_loopback let backlog = ref 64 let pid = ref None +let domains = ref None -let setup_config port' inet_addr' backlog' pid' = +let setup_config domains' port' inet_addr' backlog' pid' = port := port'; inet_addr := inet_addr'; backlog := backlog'; - pid := pid' + pid := pid'; + domains := domains' let config_from_globals () = let sockaddr = Unix.(ADDR_INET (!inet_addr, !port)) in - Vif_config.config ?pid:!pid ~backlog:!backlog sockaddr + Vif_config.config ?domains:!domains ?pid:!pid ~backlog:!backlog sockaddr open Cmdliner @@ -34,13 +36,17 @@ let inet_addr = & opt inet_addr Unix.inet_addr_loopback & info [ "i"; "inet-addr" ] ~doc ~docv:"INET_ADDR" +let is_not_directory str = + (Sys.file_exists str && Sys.is_directory str = false) + || Sys.file_exists str = false + let pid = let doc = "Specify a file to record its process-id in." in let non_existing_file = let parser str = match Fpath.of_string str with - | Ok _ as v when Sys.file_exists str = false -> v - | Ok v -> error_msgf "%a already exists" Fpath.pp v + | Ok _ as v when is_not_directory str -> v + | Ok v -> error_msgf "%a already exists as a directory" Fpath.pp v | Error _ as err -> err in Arg.conv (parser, Fpath.pp) @@ -50,6 +56,11 @@ let pid = & opt (some non_existing_file) None & info [ "pid-file" ] ~doc ~docv:"PATH" +let domains = + let doc = "The number of number used by vif." in + let open Arg in + value & opt (some int) None & info [ "domains" ] ~doc ~docv:"DOMAINS" + let backlog = let doc = "The limit of outstanding connections in the socket's listen queue." @@ -59,4 +70,4 @@ let backlog = let setup_config = let open Term in - const setup_config $ port $ inet_addr $ backlog $ pid + const setup_config $ domains $ port $ inet_addr $ backlog $ pid diff --git a/lib/vif/vif_r.ml b/lib/vif/vif_r.ml index 3c94ce0..e3e557b 100644 --- a/lib/vif/vif_r.ml +++ b/lib/vif/vif_r.ml @@ -208,7 +208,7 @@ let extract t = type ('fu, 'return) req = | Request : - Vif_method.t option * ('c, 'a) Vif_content_type.t + Vif_method.t option * ('c, 'a) Vif_t.t -> (('c, 'a) Vif_request.t -> 'r, 'r) req type 'r route = Route : ('f, 'x) req * ('x, 'r) Vif_u.t * 'f -> 'r route @@ -228,21 +228,27 @@ let rec build_info_list idx = function type request = { extract: 'c 'a. - Vif_method.t option - -> ('c, 'a) Vif_content_type.t - -> ('c, 'a) Vif_request.t option + Vif_method.t option -> ('c, 'a) Vif_t.t -> ('c, 'a) Vif_request.t option } let rec find_and_trigger : type r. - original:string -> request:request -> Re.Group.t -> r re list -> r = - fun ~original ~request subs -> function + original:string + -> uid:int + -> request:request + -> Re.Group.t + -> r re list + -> r = + fun ~original ~uid ~request subs -> function | [] -> raise Not_found | Re (Request (meth, c), f, id, ret) :: l -> + Log.debug (fun m -> + m "%S matches with %a (%03d)? %b" original Re.Group.pp subs uid + (Re.Mark.test subs id)); if Re.Mark.test subs id then match request.extract meth c with | Some request -> extract ~original ret subs (f request) - | None -> find_and_trigger ~original ~request subs l - else find_and_trigger ~original ~request subs l + | None -> find_and_trigger ~original ~uid:(succ uid) ~request subs l + else find_and_trigger ~original ~uid:(succ uid) ~request subs l let dispatch : type r c. default:((c, string) Vif_request.t -> string -> r) @@ -260,7 +266,7 @@ let dispatch : type r c. let[@warning "-8"] (Some request) = request.extract None Any in default request target | Some subs -> begin - try find_and_trigger ~original:target ~request subs wl + try find_and_trigger ~original:target ~uid:0 ~request subs wl with Not_found as exn -> Log.debug (fun m -> m "Fallback to the default route (exn: %s)" diff --git a/lib/vif/vif_request.ml b/lib/vif/vif_request.ml index dc912ff..6d01bd6 100644 --- a/lib/vif/vif_request.ml +++ b/lib/vif/vif_request.ml @@ -4,16 +4,13 @@ module Log = (val Logs.src_log src : Logs.LOG) type ('c, 'a) t = { body: [ `V1 of H1.Body.Reader.t | `V2 of H2.Body.Reader.t ] - ; encoding: ('c, 'a) Vif_content_type.t + ; encoding: ('c, 'a) Vif_t.t ; env: Vif_m.Hmap.t ; request: Vif_request0.t } let of_req0 : type c a. - encoding:(c, a) Vif_content_type.t - -> env:Vif_m.Hmap.t - -> Vif_request0.t - -> (c, a) t = + encoding:(c, a) Vif_t.t -> env:Vif_m.Hmap.t -> Vif_request0.t -> (c, a) t = fun ~encoding ~env request -> let body = Vif_request0.request_body request in { request; body; encoding; env } @@ -34,8 +31,8 @@ let destruct : type a. a Json_encoding.encoding -> Json.t -> a = let error_msgf fmt = Format.kasprintf (fun msg -> Error (`Msg msg)) fmt -let of_json : type a. - (Vif_content_type.json, a) t -> (a, [> `Msg of string ]) result = function +let of_json : type a. (Vif_t.json, a) t -> (a, [> `Msg of string ]) result = + function | { encoding= Any; _ } as req -> Ok (to_string req) | { encoding= Json; _ } as req -> let stream = stream req in diff --git a/lib/vif/vif_response.ml b/lib/vif/vif_response.ml index 1bbdbf3..0d7f283 100644 --- a/lib/vif/vif_response.ml +++ b/lib/vif/vif_response.ml @@ -72,36 +72,24 @@ let compression alg req = match alg with | `DEFLATE when can_compress "deflate" req -> let* () = set ~field:"content-encoding" "deflate" in + let* () = rem ~field:"content-length" in return true | `DEFLATE -> return false let with_stream ?compression:alg req stream = - match alg with - | Some alg -> - let* _ = compression alg req in - let field = "transfer-encoding" in - let v = "chunked" in - let* _ = add_unless_exists ~field v in - Stream stream - | None -> - let field = "transfer-encoding" in - let v = "chunked" in - let* _ = add_unless_exists ~field v in - Stream stream + let none = return false in + let* _ = Option.fold ~none ~some:(fun alg -> compression alg req) alg in + let field = "transfer-encoding" in + let v = "chunked" in + let* _ = add_unless_exists ~field v in + Stream stream let with_string ?compression:alg req str = - match alg with - | Some alg -> - let* _ = compression alg req in - let field = "content-length" in - let v = string_of_int (String.length str) in - let* _ = add_unless_exists ~field v in - String str - | None -> - let field = "content-length" in - let v = string_of_int (String.length str) in - let* _ = add_unless_exists ~field v in - String str + let field = "content-length" in + let* () = add ~field (string_of_int (String.length str)) in + let none = return false in + let* _ = Option.fold ~none ~some:(fun alg -> compression alg req) alg in + String str let response ?headers:(hdrs = []) status req0 = match Vif_request0.reqd req0 with @@ -130,7 +118,6 @@ let response ?headers:(hdrs = []) status req0 = body in let full _ = false in - (* TODO(dinosaure): content-length? *) let stop = H2.Body.Writer.close in (Sink { init; push; full; stop } : (string, unit) Stream.sink) diff --git a/lib/vif/vif_t.ml b/lib/vif/vif_t.ml new file mode 100644 index 0000000..f943a96 --- /dev/null +++ b/lib/vif/vif_t.ml @@ -0,0 +1,18 @@ +type null = Null +type json = Json +type multipart_form = Multipart_form +type stream = string Stream.source Multipart_form.elt Stream.source + +type ('c, 'a) t = + | Null : (null, unit) t + | Json_encoding : 'a Json_encoding.encoding -> (json, 'a) t + | Multipart_form_encoding : 'a Vif_multipart_form.t -> (multipart_form, 'a) t + | Multipart_form : (multipart_form, stream) t + | Json : (json, Json.t) t + | Any : ('c, string) t + +let null = Null +let json_encoding e = Json_encoding e +let m e = Multipart_form_encoding e +let json = Json +let any = Any