This commit is contained in:
pena 2026-03-08 17:45:32 +01:00
parent b06a92c7de
commit a39d735af4
25 changed files with 147 additions and 244 deletions

View file

@ -1,4 +1,4 @@
version=0.28.1 version=0.29.0
assignment-operator=end-line assignment-operator=end-line
break-cases=fit break-cases=fit
break-fun-decl=wrap break-fun-decl=wrap

View file

@ -3,4 +3,4 @@
(modules main) (modules main)
(flags (flags
(:standard -open Prelude)) (:standard -open Prelude))
(libraries drame fmt prelude scfg)) (libraries drame fmt htmlit prelude scfg))

View file

@ -1,7 +1,3 @@
open Drame
open Tyxml
open Tyxml.Html
module App_id = struct module App_id = struct
let qualifier = "org" let qualifier = "org"
@ -10,64 +6,65 @@ module App_id = struct
let application = "drame" let application = "drame"
end end
module Server = Server.Make (App_id) module Server = Drame.Server.Make (App_id)
let template_html (_request : Request.t) ~title ~body = let template_html (_request : Drame.Request.t) ~title ~body =
let styles = let open Htmlit in
List.map El.html
(fun s -> link ~rel:[ `Stylesheet ] ~href:(Fmt.str "/assets/css/%s" s) ()) ~at:[ At.lang "en" ]
[ "style.css" ] [ El.head
in [ El.title [ El.txt title ]
let head = head (Html.title title) styles in ; El.link
let body = Html.body [ main [ h1 [ title ]; body ] ] in ~at:[ At.rel "stylesheet"; At.href "/assets/css/style.css" ]
let a = [ a_lang "en" ] in ()
let tyxml_doc = html ~a head body in ]
Html_doc.of_tyxml tyxml_doc ; El.body [ El.main [ El.h1 [ El.txt title ]; body ] ]
]
let hello = let hello ~name request =
let body = txt "How are you doing?" in let title = Fmt.str "Hello %s!" name in
fun ~name -> let body = Htmlit.El.txt "How are you doing?" in
let title = Fmt.kstr txt "Hello %s!" name in let doc = template_html request ~title ~body in
fun request -> let content = Drame.Content.Html doc in
let doc = template_html request ~title ~body in
let content = Content.Html doc in
Ok content
let hello_q request =
let name = Request.query request "name" in
let name = Option.value name ~default:"World" in
let msg = Fmt.kstr txt "Hello %s!" name in
let doc = template_html request ~title:msg ~body:msg in
let content = Content.Html doc in
Ok content Ok content
let config = let hello_q request =
let title = txt "Configuration" in let name =
let body = Fmt.kstr txt "%a" Scfg.Pp.config Server.config in Drame.Request.query request "name" |> Option.value ~default:"World"
fun request ->
let doc = template_html request ~title ~body in
let content = Content.Html doc in
Ok content
let not_found =
let title = txt "404 Not Found" in
let body = txt "Ooops :S" in
fun request ->
let content = template_html request ~title ~body in
Error (Status.Not_found, content)
let style =
let sheet =
{css|body {
color: #ebb2bf;
background-color: #0f1312;
}|css}
in in
let content = Content.Unsafe { content = sheet; mimetype = Text_css } in let title = Fmt.str "Hello %s!" name in
fun _request -> Ok content let body = Htmlit.El.txt title in
let doc = template_html request ~title ~body in
let content = Drame.Content.Html doc in
Ok content
let config request =
let title = "Configuration" in
let body = Fmt.kstr Htmlit.El.txt "%a" Scfg.Pp.config Server.config in
let doc = template_html request ~title ~body in
let content = Drame.Content.Html doc in
Ok content
let not_found request =
let title = "404 Not Found" in
let body = Htmlit.El.txt "Ooops :S" in
let content = template_html request ~title ~body in
Error (Drame.Status.Not_found, content)
let style _request =
let sheet =
{css|
body {
color: #ebb2bf;
background-color: #0f1312;
}
|css}
in
let content = Drame.Content.Unsafe { content = sheet; mimetype = Text_css } in
Ok content
let handler route = let handler route =
Fmt.pr "[request] %a@\n" Route.pp route; Fmt.pr "[request] %a@\n" Drame.Route.pp route;
Fmt.flush Fmt.stdout (); Fmt.flush Fmt.stdout ();
match route with match route with
| [||] -> hello ~name:"World" | [||] -> hello ~name:"World"

View file

@ -3,39 +3,23 @@
}) {} }) {}
}: }:
let
ocamlPackages = pkgs.ocaml-ng.ocamlPackages_5_4.overrideScope (self: super: {
htmlit = ocamlPackages.buildTopkgPackage rec {
pname = "htmlit";
version = "0.2.0";
minimalOCamlVersion = "4.14.0";
src = pkgs.fetchzip {
url = "https://erratique.ch/software/htmlit/releases/htmlit-${version}.tbz";
hash = "sha256-vE6XY7INMCwQztZqKhJaxpNF0o5+NeutJM5XobshClE=";
};
};
});
in
pkgs.mkShell { pkgs.mkShell {
nativeBuildInputs = with ocamlPackages; [ nativeBuildInputs = with pkgs.ocamlPackages; [
dune_3 dune_3
findlib findlib
merlin merlin
ocaml ocaml
ocamlformat ocamlformat
odoc odoc
ocp-browser
]; ];
buildInputs = with ocamlPackages; [ buildInputs = with pkgs.ocamlPackages; [
httpcats httpcats
js_of_ocaml-compiler js_of_ocaml-compiler
miou miou
ptime ptime
htmlit htmlit
scfg scfg
tyxml
uri uri
uuidm uuidm
directories directories

View file

@ -1,18 +0,0 @@
type t =
(*
| Css of string Css.css
*)
| Html of Html_doc.t
| JavaScript of Js_of_ocaml_compiler.Javascript.program
| Txt of string
| Unsafe of
{ content : string
; mimetype : Mimetype.t
}
let to_mimetype = function
(* | Css _ -> Mimetype.Text_css *)
| Html _ -> Mimetype.Text_html
| JavaScript _ -> Mimetype.Text_javascript
| Txt _ -> Mimetype.Text_plain
| Unsafe { mimetype; _ } -> mimetype

View file

@ -1,13 +1,8 @@
type t = type t =
(* | Html of Htmlit.El.html
| Css of string Css.css
*)
| Html of Html_doc.t
| JavaScript of Js_of_ocaml_compiler.Javascript.program | JavaScript of Js_of_ocaml_compiler.Javascript.program
| Txt of string | Txt of string
| Unsafe of | Unsafe of
{ content : string { content : string
; mimetype : Mimetype.t ; mimetype : Mimetype.t
} }
val to_mimetype : t -> Mimetype.t

View file

@ -5,7 +5,6 @@
cookie cookie
form form
handler handler
html_doc
meth meth
mimetype mimetype
request request
@ -13,8 +12,8 @@
route route
server server
session session
status status)
syntax) (modules_without_implementation content handler meth response status)
(libraries (libraries
bigstringaf bigstringaf
bstr bstr
@ -24,15 +23,13 @@
h1 h1
h2 h2
httpcats httpcats
(re_export js_of_ocaml-compiler) js_of_ocaml-compiler
miou miou
miou.unix miou.unix
prelude prelude
ptime ptime
ptime.clock.os ptime.clock.os
(re_export scfg) scfg
(re_export tyxml)
(re_export tyxml.functor)
htmlit htmlit
unix unix
uri uri

View file

@ -1,6 +1,6 @@
let form request = let form request =
match Request.header request "Content-Type" with match Request.header request "Content-Type" with
| None -> Error "missing content type" | None -> Error (`Bad_request "missing content type")
| Some content_type -> ( | Some content_type -> (
match String.split_on_char ';' content_type with match String.split_on_char ';' content_type with
| "application/x-www-form-urlencoded" :: _tl -> | "application/x-www-form-urlencoded" :: _tl ->
@ -10,4 +10,4 @@ let form request =
List.map (fun (name, values) -> (name, String.concat "," values)) query List.map (fun (name, values) -> (name, String.concat "," values)) query
in in
Ok (List.sort (Pair.compare String.compare String.compare) form) Ok (List.sort (Pair.compare String.compare String.compare) form)
| _content_type -> Error "wrong content type" ) | _content_type -> Error (`Bad_request "wrong content type") )

View file

@ -1 +1,2 @@
val form : Request.t -> ((string * string) list, string) result val form :
Request.t -> ((string * string) list, [> `Bad_request of string ]) result

View file

@ -1 +0,0 @@
type t = Route.t -> Request.t -> Response.t

View file

@ -1,9 +0,0 @@
type t = string
let to_string = Fun.id
let of_tyxml doc =
let indent = false in
Fmt.str "%a@\n" (Tyxml.Html.pp ~indent ()) doc
let of_htmlit doc = Htmlit.El.to_string ~doctype:true doc

View file

@ -1,7 +0,0 @@
type t
val to_string : t -> string
val of_tyxml : Tyxml.Html.doc -> t
val of_htmlit : Htmlit.El.html -> t

View file

@ -1,21 +0,0 @@
type t =
| Connect
| Delete
| Get
| Head
| Options
| Post
| Put
| Trace
| Other of string
let of_httpcats = function
| `CONNECT -> Connect
| `DELETE -> Delete
| `GET -> Get
| `HEAD -> Head
| `OPTIONS -> Options
| `POST -> Post
| `PUT -> Put
| `TRACE -> Trace
| `Other s -> Other s

View file

@ -8,5 +8,3 @@ type t =
| Put | Put
| Trace | Trace
| Other of string | Other of string
val of_httpcats : H2.Method.t -> t

View file

@ -32,6 +32,6 @@ type t =
| Video_mp4 | Video_mp4
| Video_webm | Video_webm
val pp : Format.formatter -> t -> unit val pp : t Fmt.t
val to_string : t -> string val to_string : t -> string

View file

@ -114,6 +114,17 @@ let of_reqd reqd =
in in
let route = Uri.path target |> Route.of_string in let route = Uri.path target |> Route.of_string in
let headers = H2.Headers.to_list headers in let headers = H2.Headers.to_list headers in
let meth = Meth.of_httpcats meth in let meth : Meth.t =
match meth with
| `CONNECT -> Connect
| `DELETE -> Delete
| `GET -> Get
| `HEAD -> Head
| `OPTIONS -> Options
| `POST -> Post
| `PUT -> Put
| `TRACE -> Trace
| `Other s -> Other s
in
let get_body () = get_body reqd in let get_body () = get_body reqd in
{ route; meth; query; headers; get_body } { route; meth; query; headers; get_body }

View file

@ -1 +0,0 @@
type t = (Content.t, Status.error * Html_doc.t) Result.t

View file

@ -1 +1 @@
type t = (Content.t, Status.error * Html_doc.t) Result.t type t = (Content.t, Status.error * Htmlit.El.html) Result.t

View file

@ -1,5 +1,5 @@
type t = string array type t = string array
val pp : Format.formatter -> t -> unit val pp : t Fmt.t
val of_string : string -> t val of_string : string -> t

View file

@ -32,7 +32,9 @@ struct
let config = let config =
let filename = Fpath.(Project.config_dir / "config.scfg") in let filename = Fpath.(Project.config_dir / "config.scfg") in
match Scfg.Parse.from_file filename with match Scfg.Parse.from_file filename with
| Error (`Msg msg) -> Fmt.failwith "%s" msg | Error (`Msg _msg) ->
(* TODO: warn to say there is no config file! *)
[]
| Ok config -> config | Ok config -> config
open Scfg.Query open Scfg.Query
@ -40,7 +42,9 @@ struct
let port = let port =
let directive = get_dir "port" config in let directive = get_dir "port" config in
match directive with match directive with
| None -> Fmt.failwith "configuration file is missing a port directive" | None ->
(* TODO: warn to say we use a default port! *)
8080
| Some directive -> ( | Some directive -> (
let param = get_param_int 0 directive in let param = get_param_int 0 directive in
match param with match param with
@ -54,12 +58,18 @@ struct
let stop = Httpcats.Server.stop () let stop = Httpcats.Server.stop ()
let prepare_content content = let content_to_mimetype : Content.t -> Mimetype.t = function
let content_type = Content.to_mimetype content |> Mimetype.to_string in | Html _ -> Mimetype.Text_html
| JavaScript _ -> Mimetype.Text_javascript
| Txt _ -> Mimetype.Text_plain
| Unsafe { mimetype; _ } -> mimetype
let prepare_content (content : Content.t) =
let content_type = content_to_mimetype content |> Mimetype.to_string in
let content = let content =
match content with match content with
(* | Content.Css sheet -> Fmt.str "%a@\n" Css.pp_string_css sheet *) (* | Content.Css sheet -> Fmt.str "%a@\n" Css.pp_string_css sheet *)
| Html doc -> Html_doc.to_string doc | Html doc -> Htmlit.El.to_string ~doctype:true doc
| JavaScript program -> | JavaScript program ->
let open Js_of_ocaml_compiler in let open Js_of_ocaml_compiler in
let accept_unnamed_var = true in let accept_unnamed_var = true in
@ -83,11 +93,28 @@ struct
let status_of_response = function let status_of_response = function
| Ok _ -> `OK | Ok _ -> `OK
| Error (status, _) -> Status.to_httpcats status | Error ((status : Status.error), _) ->
begin match status with
| Bad_request -> `Bad_request
| Bad_gateway -> `Bad_gateway
| Conflict -> `Conflict
| Forbidden -> `Forbidden
| Found _url -> `Found
| Gateway_timeout -> `Gateway_timeout
| Internal_server_error -> `Internal_server_error
| Moved_permanently _url -> `Moved_permanently
| Not_found -> `Not_found
| Not_implemented -> `Not_implemented
| Request_timeout -> `Request_timeout
| See_other _url -> `See_other
| Service_unavailable -> `Service_unavailable
| Too_many_requests -> `Enhance_your_calm
| Unauthorized -> `Unauthorized
end
let status_headers_of_response = function let status_headers_of_response = function
| Ok content -> begin | Ok content ->
match Content.to_mimetype content with begin match content_to_mimetype content with
| Audio_aac | Audio_midi | Audio_mp3 | Audio_wav | Audio_weba | Image_jpeg | Audio_aac | Audio_midi | Audio_mp3 | Audio_wav | Audio_weba | Image_jpeg
| Image_png | Image_gif | Image_bmp | Image_ico | Image_svg | Image_webp | Image_png | Image_gif | Image_bmp | Image_ico | Image_svg | Image_webp
| Font_ttf | Text_css | Text_javascript | Font_ttf | Text_css | Text_javascript
@ -100,7 +127,7 @@ struct
| Application_atom_xml | Application_zip | Application_7z | Text_csv | Application_atom_xml | Application_zip | Application_7z | Text_csv
| Video_avi | Video_mp4 | Video_webm -> | Video_avi | Video_mp4 | Video_webm ->
[] []
end end
| Error | Error
( ( Status.See_other redirect ( ( Status.See_other redirect
| Found redirect | Found redirect

View file

@ -31,25 +31,17 @@ module Tbl : sig
val remove : Id.t -> unit val remove : Id.t -> unit
end = struct end = struct
include Hashtbl.Make (Id) module H = Hashtbl.Make (Id)
let tbl = create 512 let tbl = H.create 512
let mutex = Mutex.create () let mutex = Mutex.create ()
let finally () = Mutex.unlock mutex let find key = Mutex.protect mutex (fun () -> H.find_opt tbl key)
let find key = let add key value = Mutex.protect mutex (fun () -> H.replace tbl key value)
Mutex.lock mutex;
Fun.protect ~finally (fun () -> find_opt tbl key)
let add key value = let remove key = Mutex.protect mutex (fun () -> H.remove tbl key)
Mutex.lock mutex;
Fun.protect ~finally (fun () -> replace tbl key value)
let remove key =
Mutex.lock mutex;
Fun.protect ~finally (fun () -> remove tbl key)
end end
let now () = Ptime.v (Ptime_clock.now_d_ps ()) let now () = Ptime.v (Ptime_clock.now_d_ps ())
@ -76,22 +68,16 @@ let create () =
let load request = let load request =
let now = now () in let now = now () in
let valid_session = let valid_session =
let session_id = Cookie.get ~decrypt:false request "drame.session" in let ( let* ) = Option.bind in
match session_id with let* session_id = Cookie.get ~decrypt:false request "drame.session" in
| None -> None let* session_id = Id.of_string session_id in
| Some session_id -> ( let* ({ id; expires_at; _ } as session) = Tbl.find session_id in
match Id.of_string session_id with let is_valid = Ptime.is_earlier now ~than:expires_at in
| None -> None if is_valid then Some session
| Some session_id -> ( else begin
match Tbl.find session_id with Tbl.remove id;
| None -> None None
| Some { id; expires_at; _ } as session -> end
let is_valid = Ptime.is_earlier now ~than:expires_at in
if is_valid then session
else begin
Tbl.remove id;
None
end ) )
in in
match valid_session with match valid_session with
| None -> | None ->
@ -140,8 +126,7 @@ let make_send_headers session =
let max_age = Option.some @@ Ptime.Span.to_float_s max_age in let max_age = Option.some @@ Ptime.Span.to_float_s max_age in
Cookie.make_set_headers ~encrypt:false ~max_age ~key:"drame.session" ~value Cookie.make_set_headers ~encrypt:false ~max_age ~key:"drame.session" ~value
let pp fmt request = let pp fmt { id; expires_at; payload } =
let { id; expires_at; payload } = load request in
Fmt.pf fmt "id = %a ; expires_at = %a ; payload:@\n @[<v>" Id.pp id Ptime.pp Fmt.pf fmt "id = %a ; expires_at = %a ; payload:@\n @[<v>" Id.pp id Ptime.pp
expires_at; expires_at;
Fmt.list Fmt.list

View file

@ -10,7 +10,7 @@ val set : Request.t -> string -> string -> unit
val drop : Request.t -> string -> unit val drop : Request.t -> string -> unit
val pp : Format.formatter -> Request.t -> unit val pp : t Fmt.t
(* Internal *) (* Internal *)
val make_send_headers : t -> (string * string) list val make_send_headers : t -> (string * string) list

View file

@ -1,31 +0,0 @@
type error =
| Moved_permanently of string
| Found of string
| See_other of string
| Bad_request
| Unauthorized
| Forbidden
| Not_found
| Request_timeout
| Too_many_requests
| Internal_server_error
| Not_implemented
| Bad_gateway
| Service_unavailable
| Gateway_timeout
let to_httpcats = function
| Moved_permanently _url -> `Moved_permanently
| Found _url -> `Found
| See_other _url -> `See_other
| Bad_request -> `Bad_request
| Unauthorized -> `Unauthorized
| Forbidden -> `Forbidden
| Not_found -> `Not_found
| Request_timeout -> `Request_timeout
| Too_many_requests -> `Enhance_your_calm
| Internal_server_error -> `Internal_server_error
| Not_implemented -> `Not_implemented
| Bad_gateway -> `Bad_gateway
| Service_unavailable -> `Service_unavailable
| Gateway_timeout -> `Gateway_timeout

View file

@ -1,17 +1,16 @@
type error = type error =
| Moved_permanently of string
| Found of string
| See_other of string
| Bad_request
| Unauthorized
| Forbidden
| Not_found
| Request_timeout
| Too_many_requests
| Internal_server_error
| Not_implemented
| Bad_gateway | Bad_gateway
| Service_unavailable | Bad_request
| Conflict
| Forbidden
| Found of string
| Gateway_timeout | Gateway_timeout
| Internal_server_error
val to_httpcats : error -> Httpcats.Status.t | Moved_permanently of string
| Not_found
| Not_implemented
| Request_timeout
| See_other of string
| Service_unavailable
| Too_many_requests
| Unauthorized

View file

@ -1,3 +0,0 @@
let ( let* ) = Result.bind
let ( let+ ) r f = Result.map f r