An OCaml webserver, but the allocating version (vs httpz which doesnt)
1(* httpz_server.ml - Minimal CLI for httpzo static file server *)
2
3open Cmdliner
4
5(* Configuration file structure *)
6type config_file = {
7 port : int option;
8 root : string option;
9 max_content_length : int64 option;
10 max_header_size : int option;
11 max_header_count : int option;
12 max_chunk_size : int option;
13}
14
15let config_codec =
16 Tomlt.(Table.(
17 obj (fun port root max_content_length max_header_size max_header_count max_chunk_size ->
18 { port; root; max_content_length; max_header_size; max_header_count; max_chunk_size })
19 |> opt_mem "port" int ~enc:(fun c -> c.port)
20 |> opt_mem "root" string ~enc:(fun c -> c.root)
21 |> opt_mem "max_content_length" int64 ~enc:(fun c -> c.max_content_length)
22 |> opt_mem "max_header_size" int ~enc:(fun c -> c.max_header_size)
23 |> opt_mem "max_header_count" int ~enc:(fun c -> c.max_header_count)
24 |> opt_mem "max_chunk_size" int ~enc:(fun c -> c.max_chunk_size)
25 |> finish
26 ))
27
28(* Load config from XDG config directory *)
29let load_config xdg =
30 match Xdge.find_config_file xdg "config.toml" with
31 | None -> None
32 | Some path ->
33 let (_fs, path_str) = path in
34 (try Some (Tomlt_unix.decode_file_exn config_codec path_str)
35 with _ -> None)
36
37(* Command-line arguments *)
38let port_arg =
39 let doc = "TCP port to listen on." in
40 Arg.(value & opt (some int) None & info ["p"; "port"] ~docv:"PORT" ~doc)
41
42let root_arg =
43 let doc = "Document root directory to serve." in
44 Arg.(value & opt (some string) None & info ["d"; "root"] ~docv:"DIR" ~doc)
45
46(* Build limits from config file and defaults *)
47let build_limits ?config () =
48 let defaults = Httpzo.default_limits in
49 match config with
50 | None -> defaults
51 | Some c ->
52 { Httpzo.Buf_read.max_content_length =
53 Option.value c.max_content_length ~default:defaults.max_content_length
54 ; max_header_size =
55 Option.value c.max_header_size ~default:defaults.max_header_size
56 ; max_header_count =
57 Option.value c.max_header_count ~default:defaults.max_header_count
58 ; max_chunk_size =
59 Option.value c.max_chunk_size ~default:defaults.max_chunk_size
60 }
61
62(* Main server function *)
63let run_server env xdg port_opt root_opt =
64 let file_config = load_config xdg in
65 (* Priority: CLI > config file > defaults *)
66 let port =
67 match port_opt with
68 | Some p -> p
69 | None ->
70 match file_config with
71 | Some { port = Some p; _ } -> p
72 | _ -> 8080
73 in
74 let root_str =
75 match root_opt with
76 | Some r -> r
77 | None ->
78 match file_config with
79 | Some { root = Some r; _ } -> r
80 | _ -> "."
81 in
82 let limits = build_limits ?config:file_config () in
83 let fs = Eio.Stdenv.fs env in
84 let root = Eio.Path.(fs / root_str) in
85 let config = Httpzo.Server.{ port; root; limits } in
86 Eio.Switch.run @@ fun sw ->
87 Httpzo.Server.run ~net:(Eio.Stdenv.net env) ~sw config
88
89(* Cmdliner term *)
90let main_term =
91 let run port root =
92 Eio_main.run @@ fun env ->
93 let fs = Eio.Stdenv.fs env in
94 let xdg = Xdge.create fs "httpzo" in
95 run_server env xdg port root
96 in
97 Term.(const run $ port_arg $ root_arg)
98
99let cmd =
100 let doc = "Static file server using httpzo" in
101 let man = [
102 `S Manpage.s_description;
103 `P "Serves static files over HTTP/1.1 with support for:";
104 `I ("Range requests", "Partial content with byte ranges");
105 `I ("ETag", "Weak entity tags for cache validation");
106 `I ("If-None-Match", "Conditional requests returning 304");
107 `I ("Keep-alive", "Connection reuse for HTTP/1.1");
108 `S Manpage.s_files;
109 `P "Configuration is read from $(b,~/.config/httpzo/config.toml) or \
110 the path specified by $(b,XDG_CONFIG_HOME).";
111 `P "Example config.toml:";
112 `Pre "port = 8080\n\
113 root = \"/var/www/html\"\n\
114 max_content_length = 104857600";
115 `S Manpage.s_environment;
116 `P "$(b,HTTPZO_CONFIG_DIR): Override configuration directory.";
117 `P "$(b,XDG_CONFIG_HOME): XDG base directory for config files.";
118 ] in
119 let info = Cmd.info "httpzo" ~version:"0.1" ~doc ~man in
120 Cmd.v info main_term
121
122let () = exit (Cmd.eval cmd)