mirror of
https://github.com/c-cube/tiny_httpd.git
synced 2026-01-21 16:56:40 -05:00
ocamlformat
This commit is contained in:
parent
30a355da5a
commit
0908d71e19
34 changed files with 2103 additions and 1983 deletions
14
.ocamlformat
Normal file
14
.ocamlformat
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
version = 0.24.1
|
||||||
|
profile=conventional
|
||||||
|
margin=80
|
||||||
|
if-then-else=k-r
|
||||||
|
parens-ite=true
|
||||||
|
parens-tuple=multi-line-only
|
||||||
|
sequence-style=terminator
|
||||||
|
type-decl=compact
|
||||||
|
break-cases=toplevel
|
||||||
|
cases-exp-indent=2
|
||||||
|
field-space=tight-decl
|
||||||
|
leading-nested-match-parens=true
|
||||||
|
module-item-spacing=compact
|
||||||
|
quiet=true
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(executable
|
(executable
|
||||||
(name sse_server)
|
(name sse_server)
|
||||||
(modules sse_server)
|
(modules sse_server)
|
||||||
|
|
@ -17,26 +16,41 @@
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets test_output.txt)
|
(targets test_output.txt)
|
||||||
(deps (:script ./run_test.sh) ./sse_client.exe ./sse_server.exe)
|
(deps
|
||||||
(enabled_if (= %{system} "linux"))
|
(:script ./run_test.sh)
|
||||||
|
./sse_client.exe
|
||||||
|
./sse_server.exe)
|
||||||
|
(enabled_if
|
||||||
|
(= %{system} "linux"))
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(action
|
(action
|
||||||
(with-stdout-to %{targets} (run %{script}))))
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
|
(run %{script}))))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
|
(= %{system} "linux"))
|
||||||
(deps test_output.txt)
|
(deps test_output.txt)
|
||||||
(action
|
(action
|
||||||
(diff test_output.txt.expected test_output.txt)))
|
(diff test_output.txt.expected test_output.txt)))
|
||||||
|
|
||||||
; produce an embedded FS
|
; produce an embedded FS
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets vfs.ml)
|
(targets vfs.ml)
|
||||||
(deps (source_tree files) (:out test_output.txt.expected))
|
(deps
|
||||||
(enabled_if (= %{system} "linux"))
|
(source_tree files)
|
||||||
(action (run %{bin:tiny-httpd-vfs-pack} -o %{targets}
|
(:out test_output.txt.expected))
|
||||||
|
(enabled_if
|
||||||
|
(= %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(run
|
||||||
|
%{bin:tiny-httpd-vfs-pack}
|
||||||
|
-o
|
||||||
|
%{targets}
|
||||||
--mirror=files/
|
--mirror=files/
|
||||||
--file=test_out.txt,%{out}
|
--file=test_out.txt,%{out}
|
||||||
; --url=example_dot_com,http://example.com ; this breaks tests in opam sandbox 😢
|
; --url=example_dot_com,http://example.com ; this breaks tests in opam sandbox 😢
|
||||||
|
|
@ -44,7 +58,8 @@
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets vfs.ml)
|
(targets vfs.ml)
|
||||||
(enabled_if (<> %{system} "linux"))
|
(enabled_if
|
||||||
|
(<> %{system} "linux"))
|
||||||
(action
|
(action
|
||||||
(with-stdout-to
|
(with-stdout-to
|
||||||
%{targets}
|
%{targets}
|
||||||
|
|
|
||||||
121
examples/echo.ml
121
examples/echo.ml
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
module S = Tiny_httpd
|
module S = Tiny_httpd
|
||||||
|
|
||||||
let now_ = Unix.gettimeofday
|
let now_ = Unix.gettimeofday
|
||||||
|
|
@ -22,30 +21,34 @@ let middleware_stat () : S.Middleware.t * (unit -> string) =
|
||||||
total_time_ := !total_time_ +. (t4 -. t1);
|
total_time_ := !total_time_ +. (t4 -. t1);
|
||||||
parse_time_ := !parse_time_ +. (t2 -. t1);
|
parse_time_ := !parse_time_ +. (t2 -. t1);
|
||||||
build_time_ := !build_time_ +. (t3 -. t2);
|
build_time_ := !build_time_ +. (t3 -. t2);
|
||||||
write_time_ := !write_time_ +. (t4 -. t3);
|
write_time_ := !write_time_ +. (t4 -. t3))
|
||||||
)
|
|
||||||
and get_stat () =
|
and get_stat () =
|
||||||
Printf.sprintf "%d requests (average response time: %.3fms = %.3fms + %.3fms + %.3fms)"
|
Printf.sprintf
|
||||||
!n_req (!total_time_ /. float !n_req *. 1e3)
|
"%d requests (average response time: %.3fms = %.3fms + %.3fms + %.3fms)"
|
||||||
|
!n_req
|
||||||
|
(!total_time_ /. float !n_req *. 1e3)
|
||||||
(!parse_time_ /. float !n_req *. 1e3)
|
(!parse_time_ /. float !n_req *. 1e3)
|
||||||
(!build_time_ /. float !n_req *. 1e3)
|
(!build_time_ /. float !n_req *. 1e3)
|
||||||
(!write_time_ /. float !n_req *. 1e3)
|
(!write_time_ /. float !n_req *. 1e3)
|
||||||
in
|
in
|
||||||
m, get_stat
|
m, get_stat
|
||||||
|
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
let port_ = ref 8080 in
|
let port_ = ref 8080 in
|
||||||
let j = ref 32 in
|
let j = ref 32 in
|
||||||
Arg.parse (Arg.align [
|
Arg.parse
|
||||||
|
(Arg.align
|
||||||
|
[
|
||||||
"--port", Arg.Set_int port_, " set port";
|
"--port", Arg.Set_int port_, " set port";
|
||||||
"-p", Arg.Set_int port_, " set port";
|
"-p", Arg.Set_int port_, " set port";
|
||||||
"--debug", Arg.Unit (fun () -> S._enable_debug true), " enable debug";
|
"--debug", Arg.Unit (fun () -> S._enable_debug true), " enable debug";
|
||||||
"-j", Arg.Set_int j, " maximum number of connections";
|
"-j", Arg.Set_int j, " maximum number of connections";
|
||||||
]) (fun _ -> raise (Arg.Bad "")) "echo [option]*";
|
])
|
||||||
|
(fun _ -> raise (Arg.Bad ""))
|
||||||
|
"echo [option]*";
|
||||||
|
|
||||||
let server = S.create ~port:!port_ ~max_connections:!j () in
|
let server = S.create ~port:!port_ ~max_connections:!j () in
|
||||||
Tiny_httpd_camlzip.setup ~compress_above:1024 ~buf_size:(16*1024) server;
|
Tiny_httpd_camlzip.setup ~compress_above:1024 ~buf_size:(16 * 1024) server;
|
||||||
|
|
||||||
let m_stats, get_stats = middleware_stat () in
|
let m_stats, get_stats = middleware_stat () in
|
||||||
S.add_middleware server ~stage:(`Stage 1) m_stats;
|
S.add_middleware server ~stage:(`Stage 1) m_stats;
|
||||||
|
|
@ -53,7 +56,7 @@ let () =
|
||||||
(* say hello *)
|
(* say hello *)
|
||||||
S.add_route_handler ~meth:`GET server
|
S.add_route_handler ~meth:`GET server
|
||||||
S.Route.(exact "hello" @/ string @/ return)
|
S.Route.(exact "hello" @/ string @/ return)
|
||||||
(fun name _req -> S.Response.make_string (Ok ("hello " ^name ^"!\n")));
|
(fun name _req -> S.Response.make_string (Ok ("hello " ^ name ^ "!\n")));
|
||||||
|
|
||||||
(* compressed file access *)
|
(* compressed file access *)
|
||||||
S.add_route_handler ~meth:`GET server
|
S.add_route_handler ~meth:`GET server
|
||||||
|
|
@ -65,21 +68,23 @@ let () =
|
||||||
try
|
try
|
||||||
let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" path) in
|
let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" path) in
|
||||||
try
|
try
|
||||||
let s = ["Content-Type", String.trim (input_line p)] in
|
let s = [ "Content-Type", String.trim (input_line p) ] in
|
||||||
ignore @@ Unix.close_process_in p;
|
ignore @@ Unix.close_process_in p;
|
||||||
s
|
s
|
||||||
with _ -> ignore @@ Unix.close_process_in p; []
|
with _ ->
|
||||||
|
ignore @@ Unix.close_process_in p;
|
||||||
|
[]
|
||||||
with _ -> []
|
with _ -> []
|
||||||
in
|
in
|
||||||
S.Response.make_stream ~headers:mime_type (Ok str)
|
S.Response.make_stream ~headers:mime_type (Ok str));
|
||||||
);
|
|
||||||
|
|
||||||
(* echo request *)
|
(* echo request *)
|
||||||
S.add_route_handler server
|
S.add_route_handler server
|
||||||
S.Route.(exact "echo" @/ return)
|
S.Route.(exact "echo" @/ return)
|
||||||
(fun req ->
|
(fun req ->
|
||||||
let q =
|
let q =
|
||||||
S.Request.query req |> List.map (fun (k,v) -> Printf.sprintf "%S = %S" k v)
|
S.Request.query req
|
||||||
|
|> List.map (fun (k, v) -> Printf.sprintf "%S = %S" k v)
|
||||||
|> String.concat ";"
|
|> String.concat ";"
|
||||||
in
|
in
|
||||||
S.Response.make_string
|
S.Response.make_string
|
||||||
|
|
@ -89,7 +94,8 @@ let () =
|
||||||
S.add_route_handler_stream ~meth:`PUT server
|
S.add_route_handler_stream ~meth:`PUT server
|
||||||
S.Route.(exact "upload" @/ string @/ return)
|
S.Route.(exact "upload" @/ string @/ return)
|
||||||
(fun path req ->
|
(fun path req ->
|
||||||
S._debug (fun k->k "start upload %S, headers:\n%s\n\n%!" path
|
S._debug (fun k ->
|
||||||
|
k "start upload %S, headers:\n%s\n\n%!" path
|
||||||
(Format.asprintf "%a" S.Headers.pp (S.Request.headers req)));
|
(Format.asprintf "%a" S.Headers.pp (S.Request.headers req)));
|
||||||
try
|
try
|
||||||
let oc = open_out @@ "/tmp/" ^ path in
|
let oc = open_out @@ "/tmp/" ^ path in
|
||||||
|
|
@ -97,43 +103,80 @@ let () =
|
||||||
flush oc;
|
flush oc;
|
||||||
S.Response.make_string (Ok "uploaded file")
|
S.Response.make_string (Ok "uploaded file")
|
||||||
with e ->
|
with e ->
|
||||||
S.Response.fail ~code:500 "couldn't upload file: %s" (Printexc.to_string e)
|
S.Response.fail ~code:500 "couldn't upload file: %s"
|
||||||
);
|
(Printexc.to_string e));
|
||||||
|
|
||||||
(* stats *)
|
(* stats *)
|
||||||
S.add_route_handler server S.Route.(exact "stats" @/ return)
|
S.add_route_handler server
|
||||||
|
S.Route.(exact "stats" @/ return)
|
||||||
(fun _req ->
|
(fun _req ->
|
||||||
let stats = get_stats() in
|
let stats = get_stats () in
|
||||||
S.Response.make_string @@ Ok stats
|
S.Response.make_string @@ Ok stats);
|
||||||
);
|
|
||||||
|
|
||||||
(* VFS *)
|
(* VFS *)
|
||||||
Tiny_httpd_dir.add_vfs server
|
Tiny_httpd_dir.add_vfs server
|
||||||
~config:(Tiny_httpd_dir.config ~download:true
|
~config:
|
||||||
|
(Tiny_httpd_dir.config ~download:true
|
||||||
~dir_behavior:Tiny_httpd_dir.Index_or_lists ())
|
~dir_behavior:Tiny_httpd_dir.Index_or_lists ())
|
||||||
~vfs:Vfs.vfs ~prefix:"vfs";
|
~vfs:Vfs.vfs ~prefix:"vfs";
|
||||||
|
|
||||||
(* main page *)
|
(* main page *)
|
||||||
S.add_route_handler server S.Route.(return)
|
S.add_route_handler server
|
||||||
|
S.Route.(return)
|
||||||
(fun _req ->
|
(fun _req ->
|
||||||
let open Tiny_httpd_html in
|
let open Tiny_httpd_html in
|
||||||
let h = html [] [
|
let h =
|
||||||
head[][title[][txt "index of echo"]];
|
html []
|
||||||
body[][
|
[
|
||||||
h3[] [txt "welcome!"];
|
head [] [ title [] [ txt "index of echo" ] ];
|
||||||
p[] [b[] [txt "endpoints are:"]];
|
body []
|
||||||
ul[] [
|
[
|
||||||
li[][pre[][txt "/hello/:name (GET)"]];
|
h3 [] [ txt "welcome!" ];
|
||||||
li[][pre[][a[A.href "/echo/"][txt "echo"]; txt " echo back query"]];
|
p [] [ b [] [ txt "endpoints are:" ] ];
|
||||||
li[][pre[][txt "/upload/:path (PUT) to upload a file"]];
|
ul []
|
||||||
li[][pre[][txt "/zcat/:path (GET) to download a file (deflate transfer-encoding)"]];
|
[
|
||||||
li[][pre[][a[A.href "/stats/"][txt"/stats/"]; txt" (GET) to access statistics"]];
|
li [] [ pre [] [ txt "/hello/:name (GET)" ] ];
|
||||||
li[][pre[][a[A.href "/vfs/"][txt"/vfs"]; txt" (GET) to access a VFS embedded in the binary"]];
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
a [ A.href "/echo/" ] [ txt "echo" ];
|
||||||
|
txt " echo back query";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
li []
|
||||||
|
[ pre [] [ txt "/upload/:path (PUT) to upload a file" ] ];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
txt
|
||||||
|
"/zcat/:path (GET) to download a file (deflate \
|
||||||
|
transfer-encoding)";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
a [ A.href "/stats/" ] [ txt "/stats/" ];
|
||||||
|
txt " (GET) to access statistics";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
li []
|
||||||
|
[
|
||||||
|
pre []
|
||||||
|
[
|
||||||
|
a [ A.href "/vfs/" ] [ txt "/vfs" ];
|
||||||
|
txt " (GET) to access a VFS embedded in the binary";
|
||||||
|
];
|
||||||
|
];
|
||||||
|
];
|
||||||
|
];
|
||||||
]
|
]
|
||||||
]
|
in
|
||||||
] in
|
|
||||||
let s = to_string_top h in
|
let s = to_string_top h in
|
||||||
S.Response.make_string ~headers:["content-type", "text/html"] @@ Ok s);
|
S.Response.make_string ~headers:[ "content-type", "text/html" ] @@ Ok s);
|
||||||
|
|
||||||
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
|
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
|
||||||
match S.run server with
|
match S.run server with
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
let addr = ref "127.0.0.1"
|
let addr = ref "127.0.0.1"
|
||||||
let port = ref 8080
|
let port = ref 8080
|
||||||
let path = ref "/clock"
|
let path = ref "/clock"
|
||||||
|
|
||||||
let bufsize = 1024
|
let bufsize = 1024
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
Arg.parse (Arg.align [
|
Arg.parse
|
||||||
|
(Arg.align
|
||||||
|
[
|
||||||
"-h", Arg.Set_string addr, " address to connect to";
|
"-h", Arg.Set_string addr, " address to connect to";
|
||||||
"-p", Arg.Set_int port, " port to connect to";
|
"-p", Arg.Set_int port, " port to connect to";
|
||||||
"--alarm", Arg.Int (fun i->Unix.alarm i|>ignore), " set alarm (in seconds)";
|
( "--alarm",
|
||||||
]) (fun s -> path := s) "sse_client [opt]* path?";
|
Arg.Int (fun i -> Unix.alarm i |> ignore),
|
||||||
|
" set alarm (in seconds)" );
|
||||||
|
])
|
||||||
|
(fun s -> path := s)
|
||||||
|
"sse_client [opt]* path?";
|
||||||
|
|
||||||
Format.printf "connect to %s:%d@." !addr !port;
|
Format.printf "connect to %s:%d@." !addr !port;
|
||||||
let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
|
let sock = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in
|
||||||
|
|
@ -25,7 +30,8 @@ let () =
|
||||||
let buf = Bytes.create bufsize in
|
let buf = Bytes.create bufsize in
|
||||||
while !continue do
|
while !continue do
|
||||||
let n = input ic buf 0 bufsize in
|
let n = input ic buf 0 bufsize in
|
||||||
if n=0 then continue := false;
|
if n = 0 then continue := false;
|
||||||
output stdout buf 0 n; flush stdout
|
output stdout buf 0 n;
|
||||||
|
flush stdout
|
||||||
done;
|
done;
|
||||||
Format.printf "exit!@."
|
Format.printf "exit!@."
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(* serves some streams of events *)
|
(* serves some streams of events *)
|
||||||
|
|
||||||
module S = Tiny_httpd
|
module S = Tiny_httpd
|
||||||
|
|
@ -6,57 +5,68 @@ module S = Tiny_httpd
|
||||||
let port = ref 8080
|
let port = ref 8080
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
Arg.parse (Arg.align [
|
Arg.parse
|
||||||
|
(Arg.align
|
||||||
|
[
|
||||||
"-p", Arg.Set_int port, " port to listen on";
|
"-p", Arg.Set_int port, " port to listen on";
|
||||||
"--debug", Arg.Bool S._enable_debug, " toggle debug";
|
"--debug", Arg.Bool S._enable_debug, " toggle debug";
|
||||||
]) (fun _ -> ()) "sse_clock [opt*]";
|
])
|
||||||
|
(fun _ -> ())
|
||||||
|
"sse_clock [opt*]";
|
||||||
let server = S.create ~port:!port () in
|
let server = S.create ~port:!port () in
|
||||||
|
|
||||||
let extra_headers = [
|
let extra_headers =
|
||||||
|
[
|
||||||
"Access-Control-Allow-Origin", "*";
|
"Access-Control-Allow-Origin", "*";
|
||||||
"Access-Control-Allow-Methods", "POST, GET, OPTIONS";
|
"Access-Control-Allow-Methods", "POST, GET, OPTIONS";
|
||||||
] in
|
]
|
||||||
|
in
|
||||||
|
|
||||||
(* tick/tock goes the clock *)
|
(* tick/tock goes the clock *)
|
||||||
S.add_route_server_sent_handler server S.Route.(exact "clock" @/ return)
|
S.add_route_server_sent_handler server
|
||||||
|
S.Route.(exact "clock" @/ return)
|
||||||
(fun _req (module EV : S.SERVER_SENT_GENERATOR) ->
|
(fun _req (module EV : S.SERVER_SENT_GENERATOR) ->
|
||||||
S._debug (fun k->k"new connection");
|
S._debug (fun k -> k "new connection");
|
||||||
EV.set_headers extra_headers;
|
EV.set_headers extra_headers;
|
||||||
let tick = ref true in
|
let tick = ref true in
|
||||||
while true do
|
while true do
|
||||||
let now = Ptime_clock.now() in
|
let now = Ptime_clock.now () in
|
||||||
S._debug (fun k->k"send clock ev %s" (Format.asprintf "%a" Ptime.pp now));
|
S._debug (fun k ->
|
||||||
EV.send_event ~event:(if !tick then "tick" else "tock")
|
k "send clock ev %s" (Format.asprintf "%a" Ptime.pp now));
|
||||||
|
EV.send_event
|
||||||
|
~event:
|
||||||
|
(if !tick then
|
||||||
|
"tick"
|
||||||
|
else
|
||||||
|
"tock")
|
||||||
~data:(Ptime.to_rfc3339 now) ();
|
~data:(Ptime.to_rfc3339 now) ();
|
||||||
tick := not !tick;
|
tick := not !tick;
|
||||||
|
|
||||||
Unix.sleepf 1.0;
|
Unix.sleepf 1.0
|
||||||
done;
|
done);
|
||||||
);
|
|
||||||
|
|
||||||
(* just count *)
|
(* just count *)
|
||||||
S.add_route_server_sent_handler server S.Route.(exact "count" @/ return)
|
S.add_route_server_sent_handler server
|
||||||
|
S.Route.(exact "count" @/ return)
|
||||||
(fun _req (module EV : S.SERVER_SENT_GENERATOR) ->
|
(fun _req (module EV : S.SERVER_SENT_GENERATOR) ->
|
||||||
let n = ref 0 in
|
let n = ref 0 in
|
||||||
while true do
|
while true do
|
||||||
EV.send_event ~data:(string_of_int !n) ();
|
EV.send_event ~data:(string_of_int !n) ();
|
||||||
incr n;
|
incr n;
|
||||||
Unix.sleepf 0.1;
|
Unix.sleepf 0.1
|
||||||
done;
|
done);
|
||||||
);
|
S.add_route_server_sent_handler server
|
||||||
S.add_route_server_sent_handler server S.Route.(exact "count" @/ int @/ return)
|
S.Route.(exact "count" @/ int @/ return)
|
||||||
(fun n _req (module EV : S.SERVER_SENT_GENERATOR) ->
|
(fun n _req (module EV : S.SERVER_SENT_GENERATOR) ->
|
||||||
for i=0 to n do
|
for i = 0 to n do
|
||||||
EV.send_event ~data:(string_of_int i) ();
|
EV.send_event ~data:(string_of_int i) ();
|
||||||
Unix.sleepf 0.1;
|
Unix.sleepf 0.1
|
||||||
done;
|
done;
|
||||||
EV.close();
|
EV.close ());
|
||||||
);
|
|
||||||
|
|
||||||
Printf.printf "listening on http://localhost:%d/\n%!" (S.port server);
|
Printf.printf "listening on http://localhost:%d/\n%!" (S.port server);
|
||||||
match S.run server with
|
match S.run server with
|
||||||
| Ok () -> ()
|
| Ok () -> ()
|
||||||
| Error e ->
|
| Error e ->
|
||||||
Printf.eprintf "error: %s\n%!" (Printexc.to_string e); exit 1
|
Printf.eprintf "error: %s\n%!" (Printexc.to_string e);
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(** Tiny Httpd.
|
(** Tiny Httpd.
|
||||||
|
|
||||||
A small HTTP/1.1 server, in pure OCaml, along with some utilities
|
A small HTTP/1.1 server, in pure OCaml, along with some utilities
|
||||||
|
|
@ -8,13 +7,8 @@
|
||||||
*)
|
*)
|
||||||
|
|
||||||
module Buf = Tiny_httpd_buf
|
module Buf = Tiny_httpd_buf
|
||||||
|
|
||||||
module Byte_stream = Tiny_httpd_stream
|
module Byte_stream = Tiny_httpd_stream
|
||||||
|
|
||||||
include Tiny_httpd_server
|
include Tiny_httpd_server
|
||||||
|
|
||||||
module Util = Tiny_httpd_util
|
module Util = Tiny_httpd_util
|
||||||
|
|
||||||
module Dir = Tiny_httpd_dir
|
module Dir = Tiny_httpd_dir
|
||||||
|
|
||||||
module Html = Tiny_httpd_html
|
module Html = Tiny_httpd_html
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(** {1 Tiny Http Server}
|
(** {1 Tiny Http Server}
|
||||||
|
|
||||||
This library implements a very simple, basic HTTP/1.1 server using blocking
|
This library implements a very simple, basic HTTP/1.1 server using blocking
|
||||||
|
|
@ -74,7 +73,6 @@ echo:
|
||||||
|
|
||||||
*)
|
*)
|
||||||
|
|
||||||
|
|
||||||
(** {2 Tiny buffer implementation}
|
(** {2 Tiny buffer implementation}
|
||||||
|
|
||||||
These buffers are used to avoid allocating too many byte arrays when
|
These buffers are used to avoid allocating too many byte arrays when
|
||||||
|
|
@ -93,7 +91,9 @@ module Byte_stream = Tiny_httpd_stream
|
||||||
(** {2 Main Server Type} *)
|
(** {2 Main Server Type} *)
|
||||||
|
|
||||||
(** @inline *)
|
(** @inline *)
|
||||||
include module type of struct include Tiny_httpd_server end
|
include module type of struct
|
||||||
|
include Tiny_httpd_server
|
||||||
|
end
|
||||||
|
|
||||||
(** {2 Utils} *)
|
(** {2 Utils} *)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
|
type t = { mutable bytes: bytes; mutable i: int }
|
||||||
|
|
||||||
type t = {
|
let create ?(size = 4_096) () : t = { bytes = Bytes.make size ' '; i = 0 }
|
||||||
mutable bytes: bytes;
|
|
||||||
mutable i: int;
|
|
||||||
}
|
|
||||||
|
|
||||||
let create ?(size=4_096) () : t =
|
|
||||||
{ bytes=Bytes.make size ' '; i=0 }
|
|
||||||
|
|
||||||
let size self = self.i
|
let size self = self.i
|
||||||
let bytes_slice self = self.bytes
|
let bytes_slice self = self.bytes
|
||||||
|
|
||||||
let clear self : unit =
|
let clear self : unit =
|
||||||
if Bytes.length self.bytes > 4_096 * 1_024 then (
|
if Bytes.length self.bytes > 4_096 * 1_024 then
|
||||||
self.bytes <- Bytes.make 4096 ' '; (* free big buffer *)
|
self.bytes <- Bytes.make 4096 ' ' (* free big buffer *);
|
||||||
);
|
|
||||||
self.i <- 0
|
self.i <- 0
|
||||||
|
|
||||||
let resize self new_size : unit =
|
let resize self new_size : unit =
|
||||||
|
|
@ -20,16 +14,15 @@ let resize self new_size : unit =
|
||||||
Bytes.blit self.bytes 0 new_buf 0 self.i;
|
Bytes.blit self.bytes 0 new_buf 0 self.i;
|
||||||
self.bytes <- new_buf
|
self.bytes <- new_buf
|
||||||
|
|
||||||
let add_bytes (self:t) s i len : unit =
|
let add_bytes (self : t) s i len : unit =
|
||||||
if self.i + len >= Bytes.length self.bytes then (
|
if self.i + len >= Bytes.length self.bytes then
|
||||||
resize self (self.i + self.i / 2 + len + 10);
|
resize self (self.i + (self.i / 2) + len + 10);
|
||||||
);
|
|
||||||
Bytes.blit s i self.bytes self.i len;
|
Bytes.blit s i self.bytes self.i len;
|
||||||
self.i <- self.i + len
|
self.i <- self.i + len
|
||||||
|
|
||||||
let contents (self:t) : string = Bytes.sub_string self.bytes 0 self.i
|
let contents (self : t) : string = Bytes.sub_string self.bytes 0 self.i
|
||||||
|
|
||||||
let contents_and_clear (self:t) : string =
|
let contents_and_clear (self : t) : string =
|
||||||
let x = contents self in
|
let x = contents self in
|
||||||
clear self;
|
clear self;
|
||||||
x
|
x
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(** Simple buffer.
|
(** Simple buffer.
|
||||||
|
|
||||||
These buffers are used to avoid allocating too many byte arrays when
|
These buffers are used to avoid allocating too many byte arrays when
|
||||||
|
|
@ -8,6 +7,7 @@
|
||||||
*)
|
*)
|
||||||
|
|
||||||
type t
|
type t
|
||||||
|
|
||||||
val size : t -> int
|
val size : t -> int
|
||||||
val clear : t -> unit
|
val clear : t -> unit
|
||||||
val create : ?size:int -> unit -> t
|
val create : ?size:int -> unit -> t
|
||||||
|
|
@ -24,4 +24,3 @@ val contents_and_clear : t -> string
|
||||||
val add_bytes : t -> bytes -> int -> int -> unit
|
val add_bytes : t -> bytes -> int -> int -> unit
|
||||||
(** Append given bytes slice to the buffer.
|
(** Append given bytes slice to the buffer.
|
||||||
@since 0.5 *)
|
@since 0.5 *)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,62 +3,73 @@ module U = Tiny_httpd_util
|
||||||
module Html = Tiny_httpd_html
|
module Html = Tiny_httpd_html
|
||||||
module Pf = Printf
|
module Pf = Printf
|
||||||
|
|
||||||
type dir_behavior =
|
type dir_behavior = Index | Lists | Index_or_lists | Forbidden
|
||||||
| Index | Lists | Index_or_lists | Forbidden
|
|
||||||
|
|
||||||
type hidden = unit
|
type hidden = unit
|
||||||
|
|
||||||
type config = {
|
type config = {
|
||||||
mutable download: bool;
|
mutable download: bool;
|
||||||
mutable dir_behavior: dir_behavior;
|
mutable dir_behavior: dir_behavior;
|
||||||
mutable delete: bool;
|
mutable delete: bool;
|
||||||
mutable upload: bool;
|
mutable upload: bool;
|
||||||
mutable max_upload_size: int;
|
mutable max_upload_size: int;
|
||||||
_rest: hidden
|
_rest: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
let default_config_ : config =
|
let default_config_ : config =
|
||||||
{ download=true;
|
{
|
||||||
dir_behavior=Forbidden;
|
download = true;
|
||||||
delete=false;
|
dir_behavior = Forbidden;
|
||||||
upload=false;
|
delete = false;
|
||||||
|
upload = false;
|
||||||
max_upload_size = 10 * 1024 * 1024;
|
max_upload_size = 10 * 1024 * 1024;
|
||||||
_rest=();
|
_rest = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
let default_config () = default_config_
|
let default_config () = default_config_
|
||||||
let config
|
|
||||||
?(download=default_config_.download)
|
let config ?(download = default_config_.download)
|
||||||
?(dir_behavior=default_config_.dir_behavior)
|
?(dir_behavior = default_config_.dir_behavior)
|
||||||
?(delete=default_config_.delete)
|
?(delete = default_config_.delete) ?(upload = default_config_.upload)
|
||||||
?(upload=default_config_.upload)
|
?(max_upload_size = default_config_.max_upload_size) () : config =
|
||||||
?(max_upload_size=default_config_.max_upload_size)
|
{ download; dir_behavior; delete; upload; max_upload_size; _rest = () }
|
||||||
() : config =
|
|
||||||
{ download; dir_behavior; delete; upload; max_upload_size;
|
|
||||||
_rest=()}
|
|
||||||
|
|
||||||
let contains_dot_dot s =
|
let contains_dot_dot s =
|
||||||
try
|
try
|
||||||
String.iteri
|
String.iteri
|
||||||
(fun i c ->
|
(fun i c ->
|
||||||
if c='.' && i+1 < String.length s && String.get s (i+1) = '.' then raise Exit)
|
if c = '.' && i + 1 < String.length s && String.get s (i + 1) = '.' then
|
||||||
|
raise Exit)
|
||||||
s;
|
s;
|
||||||
false
|
false
|
||||||
with Exit -> true
|
with Exit -> true
|
||||||
|
|
||||||
(* Human readable size *)
|
(* Human readable size *)
|
||||||
let human_size (x:int) : string =
|
let human_size (x : int) : string =
|
||||||
if x >= 1_000_000_000 then Printf.sprintf "%d.%dG" (x / 1_000_000_000) ((x/1_000_000) mod 1_000_000)
|
if x >= 1_000_000_000 then
|
||||||
else if x >= 1_000_000 then Printf.sprintf "%d.%dM" (x / 1_000_000) ((x/1000) mod 1_000)
|
Printf.sprintf "%d.%dG" (x / 1_000_000_000) (x / 1_000_000 mod 1_000_000)
|
||||||
else if x >= 1_000 then Printf.sprintf "%d.%dk" (x/1000) ((x/100) mod 100)
|
else if x >= 1_000_000 then
|
||||||
else Printf.sprintf "%db" x
|
Printf.sprintf "%d.%dM" (x / 1_000_000) (x / 1000 mod 1_000)
|
||||||
|
else if x >= 1_000 then
|
||||||
|
Printf.sprintf "%d.%dk" (x / 1000) (x / 100 mod 100)
|
||||||
|
else
|
||||||
|
Printf.sprintf "%db" x
|
||||||
|
|
||||||
let header_html = "Content-Type", "text/html"
|
let header_html = "Content-Type", "text/html"
|
||||||
let (//) = Filename.concat
|
let ( // ) = Filename.concat
|
||||||
|
|
||||||
let encode_path s = U.percent_encode ~skip:(function '/' -> true|_->false) s
|
let encode_path s =
|
||||||
let _decode_path s = match U.percent_decode s with Some s->s | None -> s
|
U.percent_encode
|
||||||
|
~skip:(function
|
||||||
|
| '/' -> true
|
||||||
|
| _ -> false)
|
||||||
|
s
|
||||||
|
|
||||||
let is_hidden s = String.length s>0 && s.[0] = '.'
|
let _decode_path s =
|
||||||
|
match U.percent_decode s with
|
||||||
|
| Some s -> s
|
||||||
|
| None -> s
|
||||||
|
|
||||||
|
let is_hidden s = String.length s > 0 && s.[0] = '.'
|
||||||
|
|
||||||
module type VFS = sig
|
module type VFS = sig
|
||||||
val descr : string
|
val descr : string
|
||||||
|
|
@ -74,42 +85,46 @@ end
|
||||||
|
|
||||||
type vfs = (module VFS)
|
type vfs = (module VFS)
|
||||||
|
|
||||||
let vfs_of_dir (top:string) : vfs =
|
let vfs_of_dir (top : string) : vfs =
|
||||||
let module M = struct
|
let module M = struct
|
||||||
let descr = top
|
let descr = top
|
||||||
let (//) = Filename.concat
|
let ( // ) = Filename.concat
|
||||||
let is_directory f = Sys.is_directory (top // f)
|
let is_directory f = Sys.is_directory (top // f)
|
||||||
let contains f = Sys.file_exists (top // f)
|
let contains f = Sys.file_exists (top // f)
|
||||||
let list_dir f = Sys.readdir (top // f)
|
let list_dir f = Sys.readdir (top // f)
|
||||||
|
|
||||||
let read_file_content f =
|
let read_file_content f =
|
||||||
let ic = Unix.(openfile (top // f) [O_RDONLY] 0) in
|
let ic = Unix.(openfile (top // f) [ O_RDONLY ] 0) in
|
||||||
Tiny_httpd_stream.of_fd ic
|
Tiny_httpd_stream.of_fd ic
|
||||||
|
|
||||||
let create f =
|
let create f =
|
||||||
let oc = open_out_bin (top // f) in
|
let oc = open_out_bin (top // f) in
|
||||||
let write = output oc in
|
let write = output oc in
|
||||||
let close() = close_out oc in
|
let close () = close_out oc in
|
||||||
write, close
|
write, close
|
||||||
|
|
||||||
let delete f = Sys.remove (top // f)
|
let delete f = Sys.remove (top // f)
|
||||||
|
|
||||||
let file_size f =
|
let file_size f =
|
||||||
try Some (Unix.stat (top // f)).Unix.st_size
|
try Some (Unix.stat (top // f)).Unix.st_size with _ -> None
|
||||||
with _ -> None
|
|
||||||
let file_mtime f =
|
let file_mtime f =
|
||||||
try Some (Unix.stat (top // f)).Unix.st_mtime
|
try Some (Unix.stat (top // f)).Unix.st_mtime with _ -> None
|
||||||
with _ -> None
|
|
||||||
end in
|
end in
|
||||||
(module M)
|
(module M)
|
||||||
|
|
||||||
let html_list_dir (module VFS:VFS) ~prefix ~parent d : Html.elt =
|
let html_list_dir (module VFS : VFS) ~prefix ~parent d : Html.elt =
|
||||||
let entries = VFS.list_dir d in
|
let entries = VFS.list_dir d in
|
||||||
Array.sort String.compare entries;
|
Array.sort String.compare entries;
|
||||||
let open Html in
|
let open Html in
|
||||||
|
|
||||||
(* TODO: breadcrumbs for the path, each element a link to the given ancestor dir *)
|
(* TODO: breadcrumbs for the path, each element a link to the given ancestor dir *)
|
||||||
let head =
|
let head =
|
||||||
head[][
|
head []
|
||||||
title[][txtf "list directory %S" VFS.descr];
|
[
|
||||||
meta[A.charset "utf-8"];
|
title [] [ txtf "list directory %S" VFS.descr ];
|
||||||
] in
|
meta [ A.charset "utf-8" ];
|
||||||
|
]
|
||||||
|
in
|
||||||
|
|
||||||
let n_hidden = ref 0 in
|
let n_hidden = ref 0 in
|
||||||
Array.iter (fun f -> if is_hidden f then incr n_hidden) entries;
|
Array.iter (fun f -> if is_hidden f then incr n_hidden) entries;
|
||||||
|
|
@ -117,51 +132,70 @@ let html_list_dir (module VFS:VFS) ~prefix ~parent d : Html.elt =
|
||||||
let file_to_elt f : elt option =
|
let file_to_elt f : elt option =
|
||||||
if not @@ contains_dot_dot (d // f) then (
|
if not @@ contains_dot_dot (d // f) then (
|
||||||
let fpath = d // f in
|
let fpath = d // f in
|
||||||
if not @@ VFS.contains fpath then (
|
if not @@ VFS.contains fpath then
|
||||||
Some (li[][txtf "%s [invalid file]" f])
|
Some (li [] [ txtf "%s [invalid file]" f ])
|
||||||
) else (
|
else (
|
||||||
let size =
|
let size =
|
||||||
match VFS.file_size fpath with
|
match VFS.file_size fpath with
|
||||||
| Some f -> Printf.sprintf " (%s)" @@ human_size f
|
| Some f -> Printf.sprintf " (%s)" @@ human_size f
|
||||||
| None -> ""
|
| None -> ""
|
||||||
in
|
in
|
||||||
Some (li'[] [
|
Some
|
||||||
sub_e @@ a[A.href ("/" // prefix // fpath)][txt f];
|
(li' []
|
||||||
(if VFS.is_directory fpath then sub_e @@ txt "[dir]" else sub_empty);
|
[
|
||||||
|
sub_e @@ a [ A.href ("/" // prefix // fpath) ] [ txt f ];
|
||||||
|
(if VFS.is_directory fpath then
|
||||||
|
sub_e @@ txt "[dir]"
|
||||||
|
else
|
||||||
|
sub_empty);
|
||||||
sub_e @@ txt size;
|
sub_e @@ txt size;
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
) else None
|
) else
|
||||||
|
None
|
||||||
in
|
in
|
||||||
|
|
||||||
let body = body'[] [
|
let body =
|
||||||
sub_e @@ h2[][txtf "Index of %S" d];
|
body' []
|
||||||
begin match parent with
|
[
|
||||||
|
sub_e @@ h2 [] [ txtf "Index of %S" d ];
|
||||||
|
(match parent with
|
||||||
| None -> sub_empty
|
| None -> sub_empty
|
||||||
| Some p ->
|
| Some p ->
|
||||||
sub_e @@
|
sub_e
|
||||||
a[A.href (encode_path ("/" // prefix // p))][txt"(parent directory)"]
|
@@ a
|
||||||
end;
|
[ A.href (encode_path ("/" // prefix // p)) ]
|
||||||
|
[ txt "(parent directory)" ]);
|
||||||
sub_e @@ ul' [] [
|
sub_e
|
||||||
if !n_hidden>0 then
|
@@ ul' []
|
||||||
sub_e @@ details'[][
|
[
|
||||||
sub_e @@ summary[][txtf "(%d hidden files)" !n_hidden];
|
(if !n_hidden > 0 then
|
||||||
sub_seq (
|
sub_e
|
||||||
seq_of_array entries
|
@@ details' []
|
||||||
|> Seq.filter_map
|
[
|
||||||
(fun f -> if is_hidden f then file_to_elt f else None)
|
sub_e
|
||||||
);
|
@@ summary [] [ txtf "(%d hidden files)" !n_hidden ];
|
||||||
] else sub_empty;
|
sub_seq
|
||||||
sub_seq (
|
(seq_of_array entries
|
||||||
seq_of_array entries
|
|
||||||
|> Seq.filter_map (fun f ->
|
|> Seq.filter_map (fun f ->
|
||||||
if not (is_hidden f) then file_to_elt f else None)
|
if is_hidden f then
|
||||||
)
|
file_to_elt f
|
||||||
|
else
|
||||||
|
None));
|
||||||
|
]
|
||||||
|
else
|
||||||
|
sub_empty);
|
||||||
|
sub_seq
|
||||||
|
(seq_of_array entries
|
||||||
|
|> Seq.filter_map (fun f ->
|
||||||
|
if not (is_hidden f) then
|
||||||
|
file_to_elt f
|
||||||
|
else
|
||||||
|
None));
|
||||||
];
|
];
|
||||||
]
|
]
|
||||||
in
|
in
|
||||||
html [][head; body]
|
html [] [ head; body ]
|
||||||
|
|
||||||
let finally_ ~h x f =
|
let finally_ ~h x f =
|
||||||
try
|
try
|
||||||
|
|
@ -173,120 +207,135 @@ let finally_ ~h x f =
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
(* @param on_fs: if true, we assume the file exists on the FS *)
|
(* @param on_fs: if true, we assume the file exists on the FS *)
|
||||||
let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS:VFS) as vfs) ~prefix server : unit=
|
let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
|
||||||
|
: unit =
|
||||||
let route () =
|
let route () =
|
||||||
if prefix="" then S.Route.rest_of_path_urlencoded
|
if prefix = "" then
|
||||||
else S.Route.exact_path prefix S.Route.rest_of_path_urlencoded
|
S.Route.rest_of_path_urlencoded
|
||||||
|
else
|
||||||
|
S.Route.exact_path prefix S.Route.rest_of_path_urlencoded
|
||||||
in
|
in
|
||||||
if config.delete then (
|
if config.delete then
|
||||||
S.add_route_handler server ~meth:`DELETE (route())
|
S.add_route_handler server ~meth:`DELETE (route ()) (fun path _req ->
|
||||||
(fun path _req ->
|
if contains_dot_dot path then
|
||||||
if contains_dot_dot path then (
|
|
||||||
S.Response.fail_raise ~code:403 "invalid path in delete"
|
S.Response.fail_raise ~code:403 "invalid path in delete"
|
||||||
) else (
|
else
|
||||||
S.Response.make_string
|
S.Response.make_string
|
||||||
(try
|
(try
|
||||||
VFS.delete path; Ok "file deleted successfully"
|
VFS.delete path;
|
||||||
with e -> Error (500, Printexc.to_string e))
|
Ok "file deleted successfully"
|
||||||
)
|
with e -> Error (500, Printexc.to_string e)))
|
||||||
);
|
else
|
||||||
) else (
|
S.add_route_handler server ~meth:`DELETE (route ()) (fun _ _ ->
|
||||||
S.add_route_handler server ~meth:`DELETE (route())
|
S.Response.make_raw ~code:405 "delete not allowed");
|
||||||
(fun _ _ -> S.Response.make_raw ~code:405 "delete not allowed");
|
|
||||||
);
|
|
||||||
|
|
||||||
if config.upload then (
|
if config.upload then
|
||||||
S.add_route_handler_stream server ~meth:`PUT (route())
|
S.add_route_handler_stream server ~meth:`PUT (route ())
|
||||||
~accept:(fun req ->
|
~accept:(fun req ->
|
||||||
match S.Request.get_header_int req "Content-Length" with
|
match S.Request.get_header_int req "Content-Length" with
|
||||||
| Some n when n > config.max_upload_size ->
|
| Some n when n > config.max_upload_size ->
|
||||||
Error (403, "max upload size is " ^ string_of_int config.max_upload_size)
|
Error
|
||||||
|
(403, "max upload size is " ^ string_of_int config.max_upload_size)
|
||||||
| Some _ when contains_dot_dot req.S.Request.path ->
|
| Some _ when contains_dot_dot req.S.Request.path ->
|
||||||
Error (403, "invalid path (contains '..')")
|
Error (403, "invalid path (contains '..')")
|
||||||
| _ -> Ok ()
|
| _ -> Ok ())
|
||||||
)
|
|
||||||
(fun path req ->
|
(fun path req ->
|
||||||
let write, close =
|
let write, close =
|
||||||
try VFS.create path
|
try VFS.create path
|
||||||
with e ->
|
with e ->
|
||||||
S.Response.fail_raise ~code:403 "cannot upload to %S: %s"
|
S.Response.fail_raise ~code:403 "cannot upload to %S: %s" path
|
||||||
path (Printexc.to_string e)
|
(Printexc.to_string e)
|
||||||
|
in
|
||||||
|
let req =
|
||||||
|
S.Request.limit_body_size ~max_size:config.max_upload_size req
|
||||||
in
|
in
|
||||||
let req = S.Request.limit_body_size ~max_size:config.max_upload_size req in
|
|
||||||
Tiny_httpd_stream.iter write req.S.Request.body;
|
Tiny_httpd_stream.iter write req.S.Request.body;
|
||||||
close ();
|
close ();
|
||||||
S._debug (fun k->k "done uploading");
|
S._debug (fun k -> k "done uploading");
|
||||||
S.Response.make_raw ~code:201 "upload successful"
|
S.Response.make_raw ~code:201 "upload successful")
|
||||||
)
|
else
|
||||||
) else (
|
S.add_route_handler server ~meth:`PUT (route ()) (fun _ _ ->
|
||||||
S.add_route_handler server ~meth:`PUT (route())
|
S.Response.make_raw ~code:405 "upload not allowed");
|
||||||
(fun _ _ -> S.Response.make_raw ~code:405 "upload not allowed");
|
|
||||||
);
|
|
||||||
|
|
||||||
if config.download then (
|
if config.download then
|
||||||
S.add_route_handler server ~meth:`GET (route())
|
S.add_route_handler server ~meth:`GET (route ()) (fun path req ->
|
||||||
(fun path req ->
|
S._debug (fun k -> k "path=%S" path);
|
||||||
S._debug (fun k->k "path=%S" path);
|
let mtime =
|
||||||
let mtime = lazy (
|
lazy
|
||||||
match VFS.file_mtime path with
|
(match VFS.file_mtime path with
|
||||||
| None -> S.Response.fail_raise ~code:403 "Cannot access file"
|
| None -> S.Response.fail_raise ~code:403 "Cannot access file"
|
||||||
| Some t -> Printf.sprintf "mtime: %.4f" t
|
| Some t -> Printf.sprintf "mtime: %.4f" t)
|
||||||
) in
|
in
|
||||||
if contains_dot_dot path then (
|
if contains_dot_dot path then
|
||||||
S.Response.fail ~code:403 "Path is forbidden";
|
S.Response.fail ~code:403 "Path is forbidden"
|
||||||
) else if not (VFS.contains path) then (
|
else if not (VFS.contains path) then
|
||||||
S.Response.fail ~code:404 "File not found";
|
S.Response.fail ~code:404 "File not found"
|
||||||
) else if S.Request.get_header req "If-None-Match" = Some (Lazy.force mtime) then (
|
else if
|
||||||
S._debug (fun k->k "cached object %S (etag: %S)" path (Lazy.force mtime));
|
S.Request.get_header req "If-None-Match" = Some (Lazy.force mtime)
|
||||||
|
then (
|
||||||
|
S._debug (fun k ->
|
||||||
|
k "cached object %S (etag: %S)" path (Lazy.force mtime));
|
||||||
S.Response.make_raw ~code:304 ""
|
S.Response.make_raw ~code:304 ""
|
||||||
) else if VFS.is_directory path then (
|
) else if VFS.is_directory path then (
|
||||||
S._debug (fun k->k "list dir %S (topdir %S)" path VFS.descr);
|
S._debug (fun k -> k "list dir %S (topdir %S)" path VFS.descr);
|
||||||
let parent = Filename.(dirname path) in
|
let parent = Filename.(dirname path) in
|
||||||
let parent = if Filename.basename path <> "." then Some parent else None in
|
let parent =
|
||||||
|
if Filename.basename path <> "." then
|
||||||
|
Some parent
|
||||||
|
else
|
||||||
|
None
|
||||||
|
in
|
||||||
match config.dir_behavior with
|
match config.dir_behavior with
|
||||||
| Index | Index_or_lists when VFS.contains (path // "index.html") ->
|
| (Index | Index_or_lists) when VFS.contains (path // "index.html") ->
|
||||||
(* redirect using path, not full path *)
|
(* redirect using path, not full path *)
|
||||||
let new_path = "/" // prefix // path // "index.html" in
|
let new_path = "/" // prefix // path // "index.html" in
|
||||||
S._debug (fun k->k "redirect to `%s`" new_path);
|
S._debug (fun k -> k "redirect to `%s`" new_path);
|
||||||
S.Response.make_void ~code:301 ()
|
S.Response.make_void ~code:301 ()
|
||||||
~headers:S.Headers.(empty |> set "location" new_path)
|
~headers:S.Headers.(empty |> set "location" new_path)
|
||||||
| Lists | Index_or_lists ->
|
| Lists | Index_or_lists ->
|
||||||
let body = html_list_dir ~prefix vfs path ~parent |> Html.to_string_top in
|
let body =
|
||||||
|
html_list_dir ~prefix vfs path ~parent |> Html.to_string_top
|
||||||
|
in
|
||||||
S.Response.make_string
|
S.Response.make_string
|
||||||
~headers:[header_html; "ETag", Lazy.force mtime]
|
~headers:[ header_html; "ETag", Lazy.force mtime ]
|
||||||
(Ok body)
|
(Ok body)
|
||||||
| Forbidden | Index ->
|
| Forbidden | Index ->
|
||||||
S.Response.make_raw ~code:405 "listing dir not allowed"
|
S.Response.make_raw ~code:405 "listing dir not allowed"
|
||||||
) else (
|
) else (
|
||||||
try
|
try
|
||||||
let mime_type =
|
let mime_type =
|
||||||
if Filename.extension path = ".css" then (
|
if Filename.extension path = ".css" then
|
||||||
["Content-Type", "text/css"]
|
[ "Content-Type", "text/css" ]
|
||||||
) else if Filename.extension path = ".js" then (
|
else if Filename.extension path = ".js" then
|
||||||
["Content-Type", "text/javascript"]
|
[ "Content-Type", "text/javascript" ]
|
||||||
) else if on_fs then (
|
else if on_fs then (
|
||||||
(* call "file" util *)
|
(* call "file" util *)
|
||||||
try
|
try
|
||||||
let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" (top // path)) in
|
let p =
|
||||||
finally_ ~h:(fun p->ignore @@ Unix.close_process_in p) p
|
Unix.open_process_in
|
||||||
|
(Printf.sprintf "file -i -b %S" (top // path))
|
||||||
|
in
|
||||||
|
finally_
|
||||||
|
~h:(fun p -> ignore @@ Unix.close_process_in p)
|
||||||
|
p
|
||||||
(fun p ->
|
(fun p ->
|
||||||
try ["Content-Type", String.trim (input_line p)]
|
try [ "Content-Type", String.trim (input_line p) ]
|
||||||
with _ -> [])
|
with _ -> [])
|
||||||
with _ -> []
|
with _ -> []
|
||||||
) else []
|
) else
|
||||||
|
[]
|
||||||
in
|
in
|
||||||
let stream = VFS.read_file_content path in
|
let stream = VFS.read_file_content path in
|
||||||
S.Response.make_raw_stream
|
S.Response.make_raw_stream
|
||||||
~headers:(mime_type@["Etag", Lazy.force mtime])
|
~headers:(mime_type @ [ "Etag", Lazy.force mtime ])
|
||||||
~code:200 stream
|
~code:200 stream
|
||||||
with e ->
|
with e ->
|
||||||
S.Response.fail ~code:500 "error while reading file: %s" (Printexc.to_string e))
|
S.Response.fail ~code:500 "error while reading file: %s"
|
||||||
)
|
(Printexc.to_string e)
|
||||||
) else (
|
))
|
||||||
S.add_route_handler server ~meth:`GET (route())
|
else
|
||||||
(fun _ _ -> S.Response.make_raw ~code:405 "download not allowed");
|
S.add_route_handler server ~meth:`GET (route ()) (fun _ _ ->
|
||||||
);
|
S.Response.make_raw ~code:405 "download not allowed");
|
||||||
()
|
()
|
||||||
|
|
||||||
let add_vfs ~config ~vfs ~prefix server : unit =
|
let add_vfs ~config ~vfs ~prefix server : unit =
|
||||||
|
|
@ -296,43 +345,38 @@ let add_dir_path ~config ~dir ~prefix server : unit =
|
||||||
add_vfs_ ~on_fs:true ~top:dir ~config ~prefix ~vfs:(vfs_of_dir dir) server
|
add_vfs_ ~on_fs:true ~top:dir ~config ~prefix ~vfs:(vfs_of_dir dir) server
|
||||||
|
|
||||||
module Embedded_fs = struct
|
module Embedded_fs = struct
|
||||||
module Str_map = Map.Make(String)
|
module Str_map = Map.Make (String)
|
||||||
|
|
||||||
type t = {
|
type t = { mtime: float; mutable entries: entry Str_map.t }
|
||||||
mtime: float;
|
and entry = File of { content: string; mtime: float } | Dir of t
|
||||||
mutable entries: entry Str_map.t
|
|
||||||
}
|
|
||||||
|
|
||||||
and entry =
|
let create ?(mtime = Unix.gettimeofday ()) () : t =
|
||||||
| File of {
|
{ mtime; entries = Str_map.empty }
|
||||||
content: string;
|
|
||||||
mtime: float;
|
|
||||||
}
|
|
||||||
| Dir of t
|
|
||||||
|
|
||||||
let create ?(mtime=Unix.gettimeofday()) () : t = {
|
let split_path_ (path : string) : string list * string =
|
||||||
mtime;
|
|
||||||
entries=Str_map.empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
let split_path_ (path:string) : string list * string =
|
|
||||||
let basename = Filename.basename path in
|
let basename = Filename.basename path in
|
||||||
let dirname =
|
let dirname =
|
||||||
Filename.dirname path
|
Filename.dirname path |> String.split_on_char '/'
|
||||||
|> String.split_on_char '/'
|
|> List.filter (function
|
||||||
|> List.filter (function "" | "." -> false | _ -> true) in
|
| "" | "." -> false
|
||||||
|
| _ -> true)
|
||||||
|
in
|
||||||
dirname, basename
|
dirname, basename
|
||||||
|
|
||||||
let add_file ?mtime (self:t) ~path content : unit =
|
let add_file ?mtime (self : t) ~path content : unit =
|
||||||
let mtime = match mtime with Some t -> t | None -> self.mtime in
|
let mtime =
|
||||||
|
match mtime with
|
||||||
|
| Some t -> t
|
||||||
|
| None -> self.mtime
|
||||||
|
in
|
||||||
let dir_path, basename = split_path_ path in
|
let dir_path, basename = split_path_ path in
|
||||||
if List.mem ".." dir_path then (
|
if List.mem ".." dir_path then invalid_arg "add_file: '..' is not allowed";
|
||||||
invalid_arg "add_file: '..' is not allowed";
|
|
||||||
);
|
|
||||||
|
|
||||||
let rec loop self dir = match dir with
|
let rec loop self dir =
|
||||||
|
match dir with
|
||||||
| [] ->
|
| [] ->
|
||||||
self.entries <- Str_map.add basename (File {mtime; content}) self.entries
|
self.entries <-
|
||||||
|
Str_map.add basename (File { mtime; content }) self.entries
|
||||||
| d :: ds ->
|
| d :: ds ->
|
||||||
let sub =
|
let sub =
|
||||||
match Str_map.find d self.entries with
|
match Str_map.find d self.entries with
|
||||||
|
|
@ -352,49 +396,61 @@ module Embedded_fs = struct
|
||||||
(* find entry *)
|
(* find entry *)
|
||||||
let find_ self path : entry option =
|
let find_ self path : entry option =
|
||||||
let dir_path, basename = split_path_ path in
|
let dir_path, basename = split_path_ path in
|
||||||
let rec loop self dir_name = match dir_name with
|
let rec loop self dir_name =
|
||||||
|
match dir_name with
|
||||||
| [] -> (try Some (Str_map.find basename self.entries) with _ -> None)
|
| [] -> (try Some (Str_map.find basename self.entries) with _ -> None)
|
||||||
| d :: ds ->
|
| d :: ds ->
|
||||||
match Str_map.find d self.entries with
|
(match Str_map.find d self.entries with
|
||||||
| exception Not_found -> None
|
| exception Not_found -> None
|
||||||
| File _ -> None
|
| File _ -> None
|
||||||
| Dir sub -> loop sub ds
|
| Dir sub -> loop sub ds)
|
||||||
in
|
in
|
||||||
if path="" then Some (Dir self)
|
if path = "" then
|
||||||
else loop self dir_path
|
Some (Dir self)
|
||||||
|
else
|
||||||
|
loop self dir_path
|
||||||
|
|
||||||
let to_vfs self : vfs =
|
let to_vfs self : vfs =
|
||||||
let module M = struct
|
let module M = struct
|
||||||
let descr = "Embedded_fs"
|
let descr = "Embedded_fs"
|
||||||
let file_mtime p = match find_ self p with
|
|
||||||
| Some (File {mtime;_}) -> Some mtime
|
let file_mtime p =
|
||||||
|
match find_ self p with
|
||||||
|
| Some (File { mtime; _ }) -> Some mtime
|
||||||
| Some (Dir _) -> Some self.mtime
|
| Some (Dir _) -> Some self.mtime
|
||||||
| _ -> None
|
| _ -> None
|
||||||
|
|
||||||
let file_size p = match find_ self p with
|
let file_size p =
|
||||||
| Some (File {content;_}) -> Some (String.length content)
|
match find_ self p with
|
||||||
|
| Some (File { content; _ }) -> Some (String.length content)
|
||||||
| _ -> None
|
| _ -> None
|
||||||
|
|
||||||
let contains p = S._debug (fun k->k "contains %S" p); match find_ self p with
|
let contains p =
|
||||||
|
S._debug (fun k -> k "contains %S" p);
|
||||||
|
match find_ self p with
|
||||||
| Some _ -> true
|
| Some _ -> true
|
||||||
| None -> false
|
| None -> false
|
||||||
|
|
||||||
let is_directory p = match find_ self p with
|
let is_directory p =
|
||||||
|
match find_ self p with
|
||||||
| Some (Dir _) -> true
|
| Some (Dir _) -> true
|
||||||
| _ -> false
|
| _ -> false
|
||||||
|
|
||||||
let read_file_content p = match find_ self p with
|
let read_file_content p =
|
||||||
| Some (File {content;_}) -> Tiny_httpd_stream.of_string content
|
match find_ self p with
|
||||||
|
| Some (File { content; _ }) -> Tiny_httpd_stream.of_string content
|
||||||
| _ -> failwith (Printf.sprintf "no such file: %S" p)
|
| _ -> failwith (Printf.sprintf "no such file: %S" p)
|
||||||
|
|
||||||
let list_dir p = S._debug (fun k->k "list dir %S" p); match find_ self p with
|
let list_dir p =
|
||||||
|
S._debug (fun k -> k "list dir %S" p);
|
||||||
|
match find_ self p with
|
||||||
| Some (Dir sub) ->
|
| Some (Dir sub) ->
|
||||||
Str_map.fold (fun sub _ acc -> sub::acc) sub.entries [] |> Array.of_list
|
Str_map.fold (fun sub _ acc -> sub :: acc) sub.entries []
|
||||||
|
|> Array.of_list
|
||||||
| _ -> failwith (Printf.sprintf "no such directory: %S" p)
|
| _ -> failwith (Printf.sprintf "no such directory: %S" p)
|
||||||
|
|
||||||
let create _ = failwith "Embedded_fs is read-only"
|
let create _ = failwith "Embedded_fs is read-only"
|
||||||
let delete _ = failwith "Embedded_fs is read-only"
|
let delete _ = failwith "Embedded_fs is read-only"
|
||||||
|
end in
|
||||||
end in (module M)
|
(module M)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(** Serving static content from directories
|
(** Serving static content from directories
|
||||||
|
|
||||||
This module provides the same functionality as the "http_of_dir" tool.
|
This module provides the same functionality as the "http_of_dir" tool.
|
||||||
|
|
@ -12,8 +11,7 @@
|
||||||
This controls what happens when the user requests the path to
|
This controls what happens when the user requests the path to
|
||||||
a directory rather than a file. *)
|
a directory rather than a file. *)
|
||||||
type dir_behavior =
|
type dir_behavior =
|
||||||
| Index
|
| Index (** Redirect to index.html if present, else fails. *)
|
||||||
(** Redirect to index.html if present, else fails. *)
|
|
||||||
| Lists
|
| Lists
|
||||||
(** Lists content of directory. Be careful of security implications. *)
|
(** Lists content of directory. Be careful of security implications. *)
|
||||||
| Index_or_lists
|
| Index_or_lists
|
||||||
|
|
@ -27,29 +25,21 @@ type hidden
|
||||||
(** Type used to prevent users from building a config directly.
|
(** Type used to prevent users from building a config directly.
|
||||||
Use {!default_config} or {!config} instead. *)
|
Use {!default_config} or {!config} instead. *)
|
||||||
|
|
||||||
(** configuration for static file handlers. This might get
|
|
||||||
more fields over time. *)
|
|
||||||
type config = {
|
type config = {
|
||||||
mutable download: bool;
|
mutable download: bool; (** Is downloading files allowed? *)
|
||||||
(** Is downloading files allowed? *)
|
|
||||||
|
|
||||||
mutable dir_behavior: dir_behavior;
|
mutable dir_behavior: dir_behavior;
|
||||||
(** Behavior when serving a directory and not a file *)
|
(** Behavior when serving a directory and not a file *)
|
||||||
|
mutable delete: bool; (** Is deleting a file allowed? (with method DELETE) *)
|
||||||
mutable delete: bool;
|
mutable upload: bool; (** Is uploading a file allowed? (with method PUT) *)
|
||||||
(** Is deleting a file allowed? (with method DELETE) *)
|
|
||||||
|
|
||||||
mutable upload: bool;
|
|
||||||
(** Is uploading a file allowed? (with method PUT) *)
|
|
||||||
|
|
||||||
mutable max_upload_size: int;
|
mutable max_upload_size: int;
|
||||||
(** If {!upload} is true, this is the maximum size in bytes for
|
(** If {!upload} is true, this is the maximum size in bytes for
|
||||||
uploaded files. *)
|
uploaded files. *)
|
||||||
|
_rest: hidden; (** Just ignore this field. *)
|
||||||
_rest: hidden;
|
|
||||||
(** Just ignore this field. *)
|
|
||||||
}
|
}
|
||||||
|
(** configuration for static file handlers. This might get
|
||||||
|
more fields over time. *)
|
||||||
|
|
||||||
|
val default_config : unit -> config
|
||||||
(** default configuration: [
|
(** default configuration: [
|
||||||
{ download=true
|
{ download=true
|
||||||
; dir_behavior=Forbidden
|
; dir_behavior=Forbidden
|
||||||
|
|
@ -57,7 +47,6 @@ type config = {
|
||||||
; upload=false
|
; upload=false
|
||||||
; max_upload_size = 10 * 1024 * 1024
|
; max_upload_size = 10 * 1024 * 1024
|
||||||
}] *)
|
}] *)
|
||||||
val default_config : unit -> config
|
|
||||||
|
|
||||||
val config :
|
val config :
|
||||||
?download:bool ->
|
?download:bool ->
|
||||||
|
|
@ -70,14 +59,11 @@ val config :
|
||||||
(** Build a config from {!default_config}.
|
(** Build a config from {!default_config}.
|
||||||
@since 0.12 *)
|
@since 0.12 *)
|
||||||
|
|
||||||
|
val add_dir_path :
|
||||||
|
config:config -> dir:string -> prefix:string -> Tiny_httpd_server.t -> unit
|
||||||
(** [add_dirpath ~config ~dir ~prefix server] adds route handle to the
|
(** [add_dirpath ~config ~dir ~prefix server] adds route handle to the
|
||||||
[server] to serve static files in [dir] when url starts with [prefix],
|
[server] to serve static files in [dir] when url starts with [prefix],
|
||||||
using the given configuration [config]. *)
|
using the given configuration [config]. *)
|
||||||
val add_dir_path :
|
|
||||||
config:config ->
|
|
||||||
dir:string ->
|
|
||||||
prefix:string ->
|
|
||||||
Tiny_httpd_server.t -> unit
|
|
||||||
|
|
||||||
(** Virtual file system.
|
(** Virtual file system.
|
||||||
|
|
||||||
|
|
@ -125,7 +111,8 @@ val add_vfs :
|
||||||
config:config ->
|
config:config ->
|
||||||
vfs:(module VFS) ->
|
vfs:(module VFS) ->
|
||||||
prefix:string ->
|
prefix:string ->
|
||||||
Tiny_httpd_server.t -> unit
|
Tiny_httpd_server.t ->
|
||||||
|
unit
|
||||||
(** Similar to {!add_dir_path} but using a virtual file system instead.
|
(** Similar to {!add_dir_path} but using a virtual file system instead.
|
||||||
@since 0.12
|
@since 0.12
|
||||||
*)
|
*)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(** HTML combinators.
|
(** HTML combinators.
|
||||||
|
|
||||||
This module provides combinators to produce html. It doesn't enforce
|
This module provides combinators to produce html. It doesn't enforce
|
||||||
|
|
@ -7,13 +6,13 @@
|
||||||
@since 0.12
|
@since 0.12
|
||||||
*)
|
*)
|
||||||
|
|
||||||
(** @inline *)
|
|
||||||
include Tiny_httpd_html_
|
include Tiny_httpd_html_
|
||||||
|
(** @inline *)
|
||||||
|
|
||||||
(** Convert a HTML element to a string.
|
(** Convert a HTML element to a string.
|
||||||
@param top if true, add DOCTYPE at the beginning. The top element should then
|
@param top if true, add DOCTYPE at the beginning. The top element should then
|
||||||
be a "html" tag. *)
|
be a "html" tag. *)
|
||||||
let to_string ?(top=false) (self:elt) : string =
|
let to_string ?(top = false) (self : elt) : string =
|
||||||
let out = Out.create () in
|
let out = Out.create () in
|
||||||
if top then Out.add_string out "<!DOCTYPE html>\n";
|
if top then Out.add_string out "<!DOCTYPE html>\n";
|
||||||
self out;
|
self out;
|
||||||
|
|
@ -23,14 +22,18 @@ let to_string ?(top=false) (self:elt) : string =
|
||||||
This is designed for fragments of HTML that are to be injected inside
|
This is designed for fragments of HTML that are to be injected inside
|
||||||
a bigger context, as it's invalid to have multiple elements at the toplevel
|
a bigger context, as it's invalid to have multiple elements at the toplevel
|
||||||
of a HTML document. *)
|
of a HTML document. *)
|
||||||
let to_string_l (l:elt list) =
|
let to_string_l (l : elt list) =
|
||||||
let out = Out.create () in
|
let out = Out.create () in
|
||||||
List.iter (fun f -> f out; Out.add_format_nl out) l;
|
List.iter
|
||||||
|
(fun f ->
|
||||||
|
f out;
|
||||||
|
Out.add_format_nl out)
|
||||||
|
l;
|
||||||
Out.to_string out
|
Out.to_string out
|
||||||
|
|
||||||
let to_string_top = to_string ~top:true
|
let to_string_top = to_string ~top:true
|
||||||
|
|
||||||
(** Convert a HTML element to a stream. This might just convert
|
(** Convert a HTML element to a stream. This might just convert
|
||||||
it to a string first, do not assume it to be more efficient. *)
|
it to a string first, do not assume it to be more efficient. *)
|
||||||
let to_stream (self:elt) : Tiny_httpd_stream.t =
|
let to_stream (self : elt) : Tiny_httpd_stream.t =
|
||||||
Tiny_httpd_stream.of_string @@ to_string self
|
Tiny_httpd_stream.of_string @@ to_string self
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(** HTTP server.
|
(** HTTP server.
|
||||||
|
|
||||||
This module implements a very simple, basic HTTP/1.1 server using blocking
|
This module implements a very simple, basic HTTP/1.1 server using blocking
|
||||||
|
|
@ -15,14 +14,7 @@ type byte_stream = Tiny_httpd_stream.t
|
||||||
(** {2 Methods} *)
|
(** {2 Methods} *)
|
||||||
|
|
||||||
module Meth : sig
|
module Meth : sig
|
||||||
type t = [
|
type t = [ `GET | `PUT | `POST | `HEAD | `DELETE | `OPTIONS ]
|
||||||
| `GET
|
|
||||||
| `PUT
|
|
||||||
| `POST
|
|
||||||
| `HEAD
|
|
||||||
| `DELETE
|
|
||||||
| `OPTIONS
|
|
||||||
]
|
|
||||||
(** A HTTP method.
|
(** A HTTP method.
|
||||||
For now we only handle a subset of these.
|
For now we only handle a subset of these.
|
||||||
|
|
||||||
|
|
@ -47,7 +39,7 @@ module Headers : sig
|
||||||
(** Empty list of headers
|
(** Empty list of headers
|
||||||
@since 0.5 *)
|
@since 0.5 *)
|
||||||
|
|
||||||
val get : ?f:(string->string) -> string -> t -> string option
|
val get : ?f:(string -> string) -> string -> t -> string option
|
||||||
(** [get k headers] looks for the header field with key [k].
|
(** [get k headers] looks for the header field with key [k].
|
||||||
@param f if provided, will transform the value before it is returned. *)
|
@param f if provided, will transform the value before it is returned. *)
|
||||||
|
|
||||||
|
|
@ -74,10 +66,10 @@ module Request : sig
|
||||||
meth: Meth.t;
|
meth: Meth.t;
|
||||||
host: string;
|
host: string;
|
||||||
headers: Headers.t;
|
headers: Headers.t;
|
||||||
http_version: int*int;
|
http_version: int * int;
|
||||||
path: string;
|
path: string;
|
||||||
path_components: string list;
|
path_components: string list;
|
||||||
query: (string*string) list;
|
query: (string * string) list;
|
||||||
body: 'body;
|
body: 'body;
|
||||||
start_time: float;
|
start_time: float;
|
||||||
(** Obtained via [get_time_s] in {!create}
|
(** Obtained via [get_time_s] in {!create}
|
||||||
|
|
@ -105,8 +97,7 @@ module Request : sig
|
||||||
val headers : _ t -> Headers.t
|
val headers : _ t -> Headers.t
|
||||||
(** List of headers of the request, including ["Host"] *)
|
(** List of headers of the request, including ["Host"] *)
|
||||||
|
|
||||||
val get_header : ?f:(string->string) -> _ t -> string -> string option
|
val get_header : ?f:(string -> string) -> _ t -> string -> string option
|
||||||
|
|
||||||
val get_header_int : _ t -> string -> int option
|
val get_header_int : _ t -> string -> int option
|
||||||
|
|
||||||
val set_header : string -> string -> 'a t -> 'a t
|
val set_header : string -> string -> 'a t -> 'a t
|
||||||
|
|
@ -129,7 +120,7 @@ module Request : sig
|
||||||
val path : _ t -> string
|
val path : _ t -> string
|
||||||
(** Request path. *)
|
(** Request path. *)
|
||||||
|
|
||||||
val query : _ t -> (string*string) list
|
val query : _ t -> (string * string) list
|
||||||
(** Decode the query part of the {!path} field
|
(** Decode the query part of the {!path} field
|
||||||
@since 0.4 *)
|
@since 0.4 *)
|
||||||
|
|
||||||
|
|
@ -152,11 +143,15 @@ module Request : sig
|
||||||
@param buf_size initial size of underlying buffer (since 0.11) *)
|
@param buf_size initial size of underlying buffer (since 0.11) *)
|
||||||
|
|
||||||
(**/**)
|
(**/**)
|
||||||
|
|
||||||
(* for testing purpose, do not use *)
|
(* for testing purpose, do not use *)
|
||||||
module Internal_ : sig
|
module Internal_ : sig
|
||||||
val parse_req_start : ?buf:buf -> get_time_s:(unit -> float) -> byte_stream -> unit t option
|
val parse_req_start :
|
||||||
|
?buf:buf -> get_time_s:(unit -> float) -> byte_stream -> unit t option
|
||||||
|
|
||||||
val parse_body : ?buf:buf -> unit t -> byte_stream -> byte_stream t
|
val parse_body : ?buf:buf -> unit t -> byte_stream -> byte_stream t
|
||||||
end
|
end
|
||||||
|
|
||||||
(**/**)
|
(**/**)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -185,13 +180,14 @@ end
|
||||||
the client to answer a {!Request.t}*)
|
the client to answer a {!Request.t}*)
|
||||||
|
|
||||||
module Response : sig
|
module Response : sig
|
||||||
type body = [`String of string | `Stream of byte_stream | `Void]
|
type body = [ `String of string | `Stream of byte_stream | `Void ]
|
||||||
(** Body of a response, either as a simple string,
|
(** Body of a response, either as a simple string,
|
||||||
or a stream of bytes, or nothing (for server-sent events). *)
|
or a stream of bytes, or nothing (for server-sent events). *)
|
||||||
|
|
||||||
type t = private {
|
type t = private {
|
||||||
code: Response_code.t; (** HTTP response code. See {!Response_code}. *)
|
code: Response_code.t; (** HTTP response code. See {!Response_code}. *)
|
||||||
headers: Headers.t; (** Headers of the reply. Some will be set by [Tiny_httpd] automatically. *)
|
headers: Headers.t;
|
||||||
|
(** Headers of the reply. Some will be set by [Tiny_httpd] automatically. *)
|
||||||
body: body; (** Body of the response. Can be empty. *)
|
body: body; (** Body of the response. Can be empty. *)
|
||||||
}
|
}
|
||||||
(** A response to send back to a client. *)
|
(** A response to send back to a client. *)
|
||||||
|
|
@ -216,19 +212,12 @@ module Response : sig
|
||||||
(** Set the response code.
|
(** Set the response code.
|
||||||
@since 0.11 *)
|
@since 0.11 *)
|
||||||
|
|
||||||
val make_raw :
|
val make_raw : ?headers:Headers.t -> code:Response_code.t -> string -> t
|
||||||
?headers:Headers.t ->
|
|
||||||
code:Response_code.t ->
|
|
||||||
string ->
|
|
||||||
t
|
|
||||||
(** Make a response from its raw components, with a string body.
|
(** Make a response from its raw components, with a string body.
|
||||||
Use [""] to not send a body at all. *)
|
Use [""] to not send a body at all. *)
|
||||||
|
|
||||||
val make_raw_stream :
|
val make_raw_stream :
|
||||||
?headers:Headers.t ->
|
?headers:Headers.t -> code:Response_code.t -> byte_stream -> t
|
||||||
code:Response_code.t ->
|
|
||||||
byte_stream ->
|
|
||||||
t
|
|
||||||
(** Same as {!make_raw} but with a stream body. The body will be sent with
|
(** Same as {!make_raw} but with a stream body. The body will be sent with
|
||||||
the chunked transfer-encoding. *)
|
the chunked transfer-encoding. *)
|
||||||
|
|
||||||
|
|
@ -236,9 +225,7 @@ module Response : sig
|
||||||
(** Return a response without a body at all.
|
(** Return a response without a body at all.
|
||||||
@since NEXT_RELEASE *)
|
@since NEXT_RELEASE *)
|
||||||
|
|
||||||
val make :
|
val make : ?headers:Headers.t -> (body, Response_code.t * string) result -> t
|
||||||
?headers:Headers.t ->
|
|
||||||
(body, Response_code.t * string) result -> t
|
|
||||||
(** [make r] turns a result into a response.
|
(** [make r] turns a result into a response.
|
||||||
|
|
||||||
- [make (Ok body)] replies with [200] and the body.
|
- [make (Ok body)] replies with [200] and the body.
|
||||||
|
|
@ -247,17 +234,15 @@ module Response : sig
|
||||||
*)
|
*)
|
||||||
|
|
||||||
val make_string :
|
val make_string :
|
||||||
?headers:Headers.t ->
|
?headers:Headers.t -> (string, Response_code.t * string) result -> t
|
||||||
(string, Response_code.t * string) result -> t
|
|
||||||
(** Same as {!make} but with a string body. *)
|
(** Same as {!make} but with a string body. *)
|
||||||
|
|
||||||
val make_stream :
|
val make_stream :
|
||||||
?headers:Headers.t ->
|
?headers:Headers.t -> (byte_stream, Response_code.t * string) result -> t
|
||||||
(byte_stream, Response_code.t * string) result -> t
|
|
||||||
(** Same as {!make} but with a stream body. *)
|
(** Same as {!make} but with a stream body. *)
|
||||||
|
|
||||||
val fail : ?headers:Headers.t -> code:int ->
|
val fail :
|
||||||
('a, unit, string, t) format4 -> 'a
|
?headers:Headers.t -> code:int -> ('a, unit, string, t) format4 -> 'a
|
||||||
(** Make the current request fail with the given code and message.
|
(** Make the current request fail with the given code and message.
|
||||||
Example: [fail ~code:404 "oh noes, %s not found" "waldo"].
|
Example: [fail ~code:404 "oh noes, %s not found" "waldo"].
|
||||||
*)
|
*)
|
||||||
|
|
@ -308,11 +293,11 @@ module Route : sig
|
||||||
This will match the entirety of the remaining route.
|
This will match the entirety of the remaining route.
|
||||||
@since 0.7 *)
|
@since 0.7 *)
|
||||||
|
|
||||||
val (@/) : ('a, 'b) comp -> ('b, 'c) t -> ('a, 'c) t
|
val ( @/ ) : ('a, 'b) comp -> ('b, 'c) t -> ('a, 'c) t
|
||||||
(** [comp / route] matches ["foo/bar/…"] iff [comp] matches ["foo"],
|
(** [comp / route] matches ["foo/bar/…"] iff [comp] matches ["foo"],
|
||||||
and [route] matches ["bar/…"]. *)
|
and [route] matches ["bar/…"]. *)
|
||||||
|
|
||||||
val exact_path : string -> ('a,'b) t -> ('a,'b) t
|
val exact_path : string -> ('a, 'b) t -> ('a, 'b) t
|
||||||
(** [exact_path "foo/bar/..." r] is equivalent to
|
(** [exact_path "foo/bar/..." r] is equivalent to
|
||||||
[exact "foo" @/ exact "bar" @/ ... @/ r]
|
[exact "foo" @/ exact "bar" @/ ... @/ r]
|
||||||
@since 0.11 **)
|
@since 0.11 **)
|
||||||
|
|
@ -366,7 +351,7 @@ val create :
|
||||||
?addr:string ->
|
?addr:string ->
|
||||||
?port:int ->
|
?port:int ->
|
||||||
?sock:Unix.file_descr ->
|
?sock:Unix.file_descr ->
|
||||||
?middlewares:([`Encoding | `Stage of int] * Middleware.t) list ->
|
?middlewares:([ `Encoding | `Stage of int ] * Middleware.t) list ->
|
||||||
unit ->
|
unit ->
|
||||||
t
|
t
|
||||||
(** Create a new webserver.
|
(** Create a new webserver.
|
||||||
|
|
@ -416,8 +401,9 @@ val active_connections : t -> int
|
||||||
|
|
||||||
val add_decode_request_cb :
|
val add_decode_request_cb :
|
||||||
t ->
|
t ->
|
||||||
(unit Request.t -> (unit Request.t * (byte_stream -> byte_stream)) option) -> unit
|
(unit Request.t -> (unit Request.t * (byte_stream -> byte_stream)) option) ->
|
||||||
[@@deprecated "use add_middleware"]
|
unit
|
||||||
|
[@@deprecated "use add_middleware"]
|
||||||
(** Add a callback for every request.
|
(** Add a callback for every request.
|
||||||
The callback can provide a stream transformer and a new request (with
|
The callback can provide a stream transformer and a new request (with
|
||||||
modified headers, typically).
|
modified headers, typically).
|
||||||
|
|
@ -427,9 +413,9 @@ val add_decode_request_cb :
|
||||||
@deprecated use {!add_middleware} instead
|
@deprecated use {!add_middleware} instead
|
||||||
*)
|
*)
|
||||||
|
|
||||||
val add_encode_response_cb:
|
val add_encode_response_cb :
|
||||||
t -> (unit Request.t -> Response.t -> Response.t option) -> unit
|
t -> (unit Request.t -> Response.t -> Response.t option) -> unit
|
||||||
[@@deprecated "use add_middleware"]
|
[@@deprecated "use add_middleware"]
|
||||||
(** Add a callback for every request/response pair.
|
(** Add a callback for every request/response pair.
|
||||||
Similarly to {!add_encode_response_cb} the callback can return a new
|
Similarly to {!add_encode_response_cb} the callback can return a new
|
||||||
response, for example to compress it.
|
response, for example to compress it.
|
||||||
|
|
@ -440,8 +426,7 @@ val add_encode_response_cb:
|
||||||
*)
|
*)
|
||||||
|
|
||||||
val add_middleware :
|
val add_middleware :
|
||||||
stage:[`Encoding | `Stage of int] ->
|
stage:[ `Encoding | `Stage of int ] -> t -> Middleware.t -> unit
|
||||||
t -> Middleware.t -> unit
|
|
||||||
(** Add a middleware to every request/response pair.
|
(** Add a middleware to every request/response pair.
|
||||||
@param stage specify when middleware applies.
|
@param stage specify when middleware applies.
|
||||||
Encoding comes first (outermost layer), then stages in increasing order.
|
Encoding comes first (outermost layer), then stages in increasing order.
|
||||||
|
|
@ -463,7 +448,8 @@ val add_route_handler :
|
||||||
?middlewares:Middleware.t list ->
|
?middlewares:Middleware.t list ->
|
||||||
?meth:Meth.t ->
|
?meth:Meth.t ->
|
||||||
t ->
|
t ->
|
||||||
('a, string Request.t -> Response.t) Route.t -> 'a ->
|
('a, string Request.t -> Response.t) Route.t ->
|
||||||
|
'a ->
|
||||||
unit
|
unit
|
||||||
(** [add_route_handler server Route.(exact "path" @/ string @/ int @/ return) f]
|
(** [add_route_handler server Route.(exact "path" @/ string @/ int @/ return) f]
|
||||||
calls [f "foo" 42 request] when a [request] with path "path/foo/42/"
|
calls [f "foo" 42 request] when a [request] with path "path/foo/42/"
|
||||||
|
|
@ -489,7 +475,8 @@ val add_route_handler_stream :
|
||||||
?middlewares:Middleware.t list ->
|
?middlewares:Middleware.t list ->
|
||||||
?meth:Meth.t ->
|
?meth:Meth.t ->
|
||||||
t ->
|
t ->
|
||||||
('a, byte_stream Request.t -> Response.t) Route.t -> 'a ->
|
('a, byte_stream Request.t -> Response.t) Route.t ->
|
||||||
|
'a ->
|
||||||
unit
|
unit
|
||||||
(** Similar to {!add_route_handler}, but where the body of the request
|
(** Similar to {!add_route_handler}, but where the body of the request
|
||||||
is a stream of bytes that has not been read yet.
|
is a stream of bytes that has not been read yet.
|
||||||
|
|
@ -517,11 +504,7 @@ module type SERVER_SENT_GENERATOR = sig
|
||||||
already sent too). *)
|
already sent too). *)
|
||||||
|
|
||||||
val send_event :
|
val send_event :
|
||||||
?event:string ->
|
?event:string -> ?id:string -> ?retry:string -> data:string -> unit -> unit
|
||||||
?id:string ->
|
|
||||||
?retry:string ->
|
|
||||||
data:string ->
|
|
||||||
unit -> unit
|
|
||||||
(** Send an event from the server.
|
(** Send an event from the server.
|
||||||
If data is a multiline string, it will be sent on separate "data:" lines. *)
|
If data is a multiline string, it will be sent on separate "data:" lines. *)
|
||||||
|
|
||||||
|
|
@ -537,7 +520,8 @@ type server_sent_generator = (module SERVER_SENT_GENERATOR)
|
||||||
val add_route_server_sent_handler :
|
val add_route_server_sent_handler :
|
||||||
?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
|
?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
|
||||||
t ->
|
t ->
|
||||||
('a, string Request.t -> server_sent_generator -> unit) Route.t -> 'a ->
|
('a, string Request.t -> server_sent_generator -> unit) Route.t ->
|
||||||
|
'a ->
|
||||||
unit
|
unit
|
||||||
(** Add a handler on an endpoint, that serves server-sent events.
|
(** Add a handler on an endpoint, that serves server-sent events.
|
||||||
|
|
||||||
|
|
@ -568,7 +552,9 @@ val run : t -> (unit, exn) result
|
||||||
|
|
||||||
(**/**)
|
(**/**)
|
||||||
|
|
||||||
val _debug : ((('a, out_channel, unit, unit, unit, unit) format6 -> 'a) -> unit) -> unit
|
val _debug :
|
||||||
val _enable_debug: bool -> unit
|
((('a, out_channel, unit, unit, unit, unit) format6 -> 'a) -> unit) -> unit
|
||||||
|
|
||||||
|
val _enable_debug : bool -> unit
|
||||||
|
|
||||||
(**/**)
|
(**/**)
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,52 @@
|
||||||
|
|
||||||
module Buf = Tiny_httpd_buf
|
module Buf = Tiny_httpd_buf
|
||||||
|
|
||||||
let spf = Printf.sprintf
|
let spf = Printf.sprintf
|
||||||
|
|
||||||
type hidden = unit
|
type hidden = unit
|
||||||
|
|
||||||
type t = {
|
type t = {
|
||||||
mutable bs: bytes;
|
mutable bs: bytes;
|
||||||
mutable off : int;
|
mutable off: int;
|
||||||
mutable len : int;
|
mutable len: int;
|
||||||
fill_buf: unit -> unit;
|
fill_buf: unit -> unit;
|
||||||
consume: int -> unit;
|
consume: int -> unit;
|
||||||
close: unit -> unit;
|
close: unit -> unit;
|
||||||
_rest: hidden;
|
_rest: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
let[@inline] close self = self.close()
|
let[@inline] close self = self.close ()
|
||||||
|
|
||||||
let empty = {
|
let empty =
|
||||||
bs=Bytes.empty;
|
{
|
||||||
off=0;
|
bs = Bytes.empty;
|
||||||
len=0;
|
off = 0;
|
||||||
fill_buf=ignore;
|
len = 0;
|
||||||
consume=ignore;
|
fill_buf = ignore;
|
||||||
close=ignore;
|
consume = ignore;
|
||||||
_rest=();
|
close = ignore;
|
||||||
}
|
_rest = ();
|
||||||
|
}
|
||||||
|
|
||||||
let make ?(bs=Bytes.create @@ 16 * 1024) ?(close=ignore) ~consume ~fill () : t =
|
let make ?(bs = Bytes.create @@ (16 * 1024)) ?(close = ignore) ~consume ~fill ()
|
||||||
let rec self = {
|
: t =
|
||||||
|
let rec self =
|
||||||
|
{
|
||||||
bs;
|
bs;
|
||||||
off=0;
|
off = 0;
|
||||||
len=0;
|
len = 0;
|
||||||
close=(fun () -> close self);
|
close = (fun () -> close self);
|
||||||
fill_buf=(fun () ->
|
fill_buf = (fun () -> if self.len = 0 then fill self);
|
||||||
if self.len = 0 then fill self);
|
consume =
|
||||||
consume=
|
|
||||||
(fun n ->
|
(fun n ->
|
||||||
assert (n <= self.len);
|
assert (n <= self.len);
|
||||||
consume self n
|
consume self n);
|
||||||
);
|
_rest = ();
|
||||||
_rest=();
|
}
|
||||||
} in
|
in
|
||||||
self
|
self
|
||||||
|
|
||||||
let of_chan_ ?(buf_size=16 * 1024) ~close ic : t =
|
let of_chan_ ?(buf_size = 16 * 1024) ~close ic : t =
|
||||||
make
|
make ~bs:(Bytes.create buf_size)
|
||||||
~bs:(Bytes.create buf_size)
|
|
||||||
~close:(fun _ -> close ic)
|
~close:(fun _ -> close ic)
|
||||||
~consume:(fun self n ->
|
~consume:(fun self n ->
|
||||||
self.off <- self.off + n;
|
self.off <- self.off + n;
|
||||||
|
|
@ -53,17 +54,15 @@ let of_chan_ ?(buf_size=16 * 1024) ~close ic : t =
|
||||||
~fill:(fun self ->
|
~fill:(fun self ->
|
||||||
if self.off >= self.len then (
|
if self.off >= self.len then (
|
||||||
self.off <- 0;
|
self.off <- 0;
|
||||||
self.len <- input ic self.bs 0 (Bytes.length self.bs);
|
self.len <- input ic self.bs 0 (Bytes.length self.bs)
|
||||||
)
|
))
|
||||||
)
|
|
||||||
()
|
()
|
||||||
|
|
||||||
let of_chan = of_chan_ ~close:close_in
|
let of_chan = of_chan_ ~close:close_in
|
||||||
let of_chan_close_noerr = of_chan_ ~close:close_in_noerr
|
let of_chan_close_noerr = of_chan_ ~close:close_in_noerr
|
||||||
|
|
||||||
let of_fd_ ?(buf_size=16 * 1024) ~close ic : t =
|
let of_fd_ ?(buf_size = 16 * 1024) ~close ic : t =
|
||||||
make
|
make ~bs:(Bytes.create buf_size)
|
||||||
~bs:(Bytes.create buf_size)
|
|
||||||
~close:(fun _ -> close ic)
|
~close:(fun _ -> close ic)
|
||||||
~consume:(fun self n ->
|
~consume:(fun self n ->
|
||||||
self.off <- self.off + n;
|
self.off <- self.off + n;
|
||||||
|
|
@ -71,28 +70,27 @@ let of_fd_ ?(buf_size=16 * 1024) ~close ic : t =
|
||||||
~fill:(fun self ->
|
~fill:(fun self ->
|
||||||
if self.off >= self.len then (
|
if self.off >= self.len then (
|
||||||
self.off <- 0;
|
self.off <- 0;
|
||||||
self.len <- Unix.read ic self.bs 0 (Bytes.length self.bs);
|
self.len <- Unix.read ic self.bs 0 (Bytes.length self.bs)
|
||||||
)
|
))
|
||||||
)
|
|
||||||
()
|
()
|
||||||
|
|
||||||
let of_fd = of_fd_ ~close:Unix.close
|
let of_fd = of_fd_ ~close:Unix.close
|
||||||
let of_fd_close_noerr = of_fd_ ~close:(fun f -> try Unix.close f with _ -> ())
|
let of_fd_close_noerr = of_fd_ ~close:(fun f -> try Unix.close f with _ -> ())
|
||||||
|
|
||||||
let rec iter f (self:t) : unit =
|
let rec iter f (self : t) : unit =
|
||||||
self.fill_buf();
|
self.fill_buf ();
|
||||||
if self.len=0 then (
|
if self.len = 0 then
|
||||||
self.close();
|
self.close ()
|
||||||
) else (
|
else (
|
||||||
f self.bs self.off self.len;
|
f self.bs self.off self.len;
|
||||||
self.consume self.len;
|
self.consume self.len;
|
||||||
(iter [@tailcall]) f self
|
(iter [@tailcall]) f self
|
||||||
)
|
)
|
||||||
|
|
||||||
let to_chan (oc:out_channel) (self:t) =
|
let to_chan (oc : out_channel) (self : t) =
|
||||||
iter (fun s i len -> output oc s i len) self
|
iter (fun s i len -> output oc s i len) self
|
||||||
|
|
||||||
let of_bytes ?(i=0) ?len (bs:bytes) : t =
|
let of_bytes ?(i = 0) ?len (bs : bytes) : t =
|
||||||
(* invariant: !i+!len is constant *)
|
(* invariant: !i+!len is constant *)
|
||||||
let len =
|
let len =
|
||||||
match len with
|
match len with
|
||||||
|
|
@ -102,25 +100,22 @@ let of_bytes ?(i=0) ?len (bs:bytes) : t =
|
||||||
| None -> Bytes.length bs - i
|
| None -> Bytes.length bs - i
|
||||||
in
|
in
|
||||||
let self =
|
let self =
|
||||||
make
|
make ~bs ~fill:ignore
|
||||||
~bs ~fill:ignore
|
|
||||||
~close:(fun self -> self.len <- 0)
|
~close:(fun self -> self.len <- 0)
|
||||||
~consume:(fun self n ->
|
~consume:(fun self n ->
|
||||||
assert (n>=0 && n<= self.len);
|
assert (n >= 0 && n <= self.len);
|
||||||
self.off <- n + self.off;
|
self.off <- n + self.off;
|
||||||
self.len <- self.len - n
|
self.len <- self.len - n)
|
||||||
)
|
|
||||||
()
|
()
|
||||||
in
|
in
|
||||||
self.off <- i;
|
self.off <- i;
|
||||||
self.len <- len;
|
self.len <- len;
|
||||||
self
|
self
|
||||||
|
|
||||||
let of_string s : t =
|
let of_string s : t = of_bytes (Bytes.unsafe_of_string s)
|
||||||
of_bytes (Bytes.unsafe_of_string s)
|
|
||||||
|
|
||||||
let with_file ?buf_size file f =
|
let with_file ?buf_size file f =
|
||||||
let ic = Unix.(openfile file [O_RDONLY] 0) in
|
let ic = Unix.(openfile file [ O_RDONLY ] 0) in
|
||||||
try
|
try
|
||||||
let x = f (of_fd ?buf_size ic) in
|
let x = f (of_fd ?buf_size ic) in
|
||||||
Unix.close ic;
|
Unix.close ic;
|
||||||
|
|
@ -129,81 +124,78 @@ let with_file ?buf_size file f =
|
||||||
Unix.close ic;
|
Unix.close ic;
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
let read_all ?(buf=Buf.create()) (self:t) : string =
|
let read_all ?(buf = Buf.create ()) (self : t) : string =
|
||||||
let continue = ref true in
|
let continue = ref true in
|
||||||
while !continue do
|
while !continue do
|
||||||
self.fill_buf();
|
self.fill_buf ();
|
||||||
if self.len > 0 then (
|
if self.len > 0 then (
|
||||||
Buf.add_bytes buf self.bs self.off self.len;
|
Buf.add_bytes buf self.bs self.off self.len;
|
||||||
self.consume self.len;
|
self.consume self.len
|
||||||
);
|
);
|
||||||
assert (self.len >= 0);
|
assert (self.len >= 0);
|
||||||
if self.len = 0 then (
|
if self.len = 0 then continue := false
|
||||||
continue := false
|
|
||||||
)
|
|
||||||
done;
|
done;
|
||||||
Buf.contents_and_clear buf
|
Buf.contents_and_clear buf
|
||||||
|
|
||||||
(* put [n] bytes from the input into bytes *)
|
(* put [n] bytes from the input into bytes *)
|
||||||
let read_exactly_ ~too_short (self:t) (bytes:bytes) (n:int) : unit =
|
let read_exactly_ ~too_short (self : t) (bytes : bytes) (n : int) : unit =
|
||||||
assert (Bytes.length bytes >= n);
|
assert (Bytes.length bytes >= n);
|
||||||
let offset = ref 0 in
|
let offset = ref 0 in
|
||||||
while !offset < n do
|
while !offset < n do
|
||||||
self.fill_buf();
|
self.fill_buf ();
|
||||||
let n_read = min self.len (n - !offset) in
|
let n_read = min self.len (n - !offset) in
|
||||||
Bytes.blit self.bs self.off bytes !offset n_read;
|
Bytes.blit self.bs self.off bytes !offset n_read;
|
||||||
offset := !offset + n_read;
|
offset := !offset + n_read;
|
||||||
self.consume n_read;
|
self.consume n_read;
|
||||||
if n_read=0 then too_short();
|
if n_read = 0 then too_short ()
|
||||||
done
|
done
|
||||||
|
|
||||||
(* read a line into the buffer, after clearing it. *)
|
(* read a line into the buffer, after clearing it. *)
|
||||||
let read_line_into (self:t) ~buf : unit =
|
let read_line_into (self : t) ~buf : unit =
|
||||||
Buf.clear buf;
|
Buf.clear buf;
|
||||||
let continue = ref true in
|
let continue = ref true in
|
||||||
while !continue do
|
while !continue do
|
||||||
self.fill_buf();
|
self.fill_buf ();
|
||||||
if self.len=0 then (
|
if self.len = 0 then (
|
||||||
continue := false;
|
continue := false;
|
||||||
if Buf.size buf = 0 then raise End_of_file;
|
if Buf.size buf = 0 then raise End_of_file
|
||||||
);
|
);
|
||||||
let j = ref self.off in
|
let j = ref self.off in
|
||||||
while !j < self.off + self.len && Bytes.get self.bs !j <> '\n' do
|
while !j < self.off + self.len && Bytes.get self.bs !j <> '\n' do
|
||||||
incr j
|
incr j
|
||||||
done;
|
done;
|
||||||
if !j-self.off < self.len then (
|
if !j - self.off < self.len then (
|
||||||
assert (Bytes.get self.bs !j = '\n');
|
assert (Bytes.get self.bs !j = '\n');
|
||||||
Buf.add_bytes buf self.bs self.off (!j-self.off); (* without \n *)
|
Buf.add_bytes buf self.bs self.off (!j - self.off);
|
||||||
self.consume (!j-self.off+1); (* remove \n *)
|
(* without \n *)
|
||||||
|
self.consume (!j - self.off + 1);
|
||||||
|
(* remove \n *)
|
||||||
continue := false
|
continue := false
|
||||||
) else (
|
) else (
|
||||||
Buf.add_bytes buf self.bs self.off self.len;
|
Buf.add_bytes buf self.bs self.off self.len;
|
||||||
self.consume self.len;
|
self.consume self.len
|
||||||
)
|
)
|
||||||
done
|
done
|
||||||
|
|
||||||
(* new stream with maximum size [max_size].
|
(* new stream with maximum size [max_size].
|
||||||
@param close_rec if true, closing this will also close the input stream
|
@param close_rec if true, closing this will also close the input stream
|
||||||
@param too_big called with read size if the max size is reached *)
|
@param too_big called with read size if the max size is reached *)
|
||||||
let limit_size_to ~close_rec ~max_size ~too_big (arg:t) : t =
|
let limit_size_to ~close_rec ~max_size ~too_big (arg : t) : t =
|
||||||
let size = ref 0 in
|
let size = ref 0 in
|
||||||
let continue = ref true in
|
let continue = ref true in
|
||||||
make
|
make ~bs:Bytes.empty
|
||||||
~bs:Bytes.empty
|
~close:(fun _ -> if close_rec then arg.close ())
|
||||||
~close:(fun _ ->
|
|
||||||
if close_rec then arg.close ())
|
|
||||||
~fill:(fun res ->
|
~fill:(fun res ->
|
||||||
if res.len = 0 && !continue then (
|
if res.len = 0 && !continue then (
|
||||||
arg.fill_buf();
|
arg.fill_buf ();
|
||||||
res.bs <- arg.bs;
|
res.bs <- arg.bs;
|
||||||
res.off <- arg.off;
|
res.off <- arg.off;
|
||||||
res.len <- arg.len;
|
res.len <- arg.len
|
||||||
) else (
|
) else (
|
||||||
arg.bs <- Bytes.empty;
|
arg.bs <- Bytes.empty;
|
||||||
arg.off <- 0;
|
arg.off <- 0;
|
||||||
arg.len <- 0;
|
arg.len <- 0
|
||||||
)
|
))
|
||||||
)
|
|
||||||
~consume:(fun res n ->
|
~consume:(fun res n ->
|
||||||
size := !size + n;
|
size := !size + n;
|
||||||
if !size > max_size then (
|
if !size > max_size then (
|
||||||
|
|
@ -212,15 +204,15 @@ let limit_size_to ~close_rec ~max_size ~too_big (arg:t) : t =
|
||||||
) else (
|
) else (
|
||||||
arg.consume n;
|
arg.consume n;
|
||||||
res.off <- res.off + n;
|
res.off <- res.off + n;
|
||||||
res.len <- res.len - n;
|
res.len <- res.len - n
|
||||||
))
|
))
|
||||||
()
|
()
|
||||||
|
|
||||||
(* read exactly [size] bytes from the stream *)
|
(* read exactly [size] bytes from the stream *)
|
||||||
let read_exactly ~close_rec ~size ~too_short (arg:t) : t =
|
let read_exactly ~close_rec ~size ~too_short (arg : t) : t =
|
||||||
if size=0 then (
|
if size = 0 then
|
||||||
empty
|
empty
|
||||||
) else (
|
else (
|
||||||
let size = ref size in
|
let size = ref size in
|
||||||
make ~bs:Bytes.empty
|
make ~bs:Bytes.empty
|
||||||
~fill:(fun res ->
|
~fill:(fun res ->
|
||||||
|
|
@ -228,53 +220,52 @@ let read_exactly ~close_rec ~size ~too_short (arg:t) : t =
|
||||||
if !size = 0 then (
|
if !size = 0 then (
|
||||||
res.bs <- Bytes.empty;
|
res.bs <- Bytes.empty;
|
||||||
res.off <- 0;
|
res.off <- 0;
|
||||||
res.len <- 0;
|
res.len <- 0
|
||||||
) else (
|
) else (
|
||||||
arg.fill_buf();
|
arg.fill_buf ();
|
||||||
res.bs <- arg.bs;
|
res.bs <- arg.bs;
|
||||||
res.off <- arg.off;
|
res.off <- arg.off;
|
||||||
let len = min arg.len !size in
|
let len = min arg.len !size in
|
||||||
if len = 0 && !size > 0 then (
|
if len = 0 && !size > 0 then too_short !size;
|
||||||
too_short !size;
|
res.len <- len
|
||||||
);
|
|
||||||
res.len <- len;
|
|
||||||
))
|
))
|
||||||
~close:(fun _res ->
|
~close:(fun _res ->
|
||||||
(* close underlying stream if [close_rec] *)
|
(* close underlying stream if [close_rec] *)
|
||||||
if close_rec then arg.close();
|
if close_rec then arg.close ();
|
||||||
size := 0
|
size := 0)
|
||||||
)
|
|
||||||
~consume:(fun res n ->
|
~consume:(fun res n ->
|
||||||
let n = min n !size in
|
let n = min n !size in
|
||||||
size := !size - n;
|
size := !size - n;
|
||||||
arg.consume n;
|
arg.consume n;
|
||||||
res.off <- res.off + n;
|
res.off <- res.off + n;
|
||||||
res.len <- res.len - n;
|
res.len <- res.len - n)
|
||||||
)
|
|
||||||
()
|
()
|
||||||
)
|
)
|
||||||
|
|
||||||
let read_line ?(buf=Buf.create()) self : string =
|
let read_line ?(buf = Buf.create ()) self : string =
|
||||||
read_line_into self ~buf;
|
read_line_into self ~buf;
|
||||||
Buf.contents buf
|
Buf.contents buf
|
||||||
|
|
||||||
let read_chunked ?(buf=Buf.create()) ~fail (bs:t) : t=
|
let read_chunked ?(buf = Buf.create ()) ~fail (bs : t) : t =
|
||||||
let first = ref true in
|
let first = ref true in
|
||||||
let read_next_chunk_len () : int =
|
let read_next_chunk_len () : int =
|
||||||
if !first then (
|
if !first then
|
||||||
first := false
|
first := false
|
||||||
) else (
|
else (
|
||||||
let line = read_line ~buf bs in
|
let line = read_line ~buf bs in
|
||||||
if String.trim line <> "" then raise (fail "expected crlf between chunks";)
|
if String.trim line <> "" then raise (fail "expected crlf between chunks")
|
||||||
);
|
);
|
||||||
let line = read_line ~buf bs in
|
let line = read_line ~buf bs in
|
||||||
(* parse chunk length, ignore extensions *)
|
(* parse chunk length, ignore extensions *)
|
||||||
let chunk_size = (
|
let chunk_size =
|
||||||
if String.trim line = "" then 0
|
if String.trim line = "" then
|
||||||
else
|
0
|
||||||
|
else (
|
||||||
try Scanf.sscanf line "%x %s@\r" (fun n _ext -> n)
|
try Scanf.sscanf line "%x %s@\r" (fun n _ext -> n)
|
||||||
with _ -> raise (fail (spf "cannot read chunk size from line %S" line))
|
with _ ->
|
||||||
) in
|
raise (fail (spf "cannot read chunk size from line %S" line))
|
||||||
|
)
|
||||||
|
in
|
||||||
chunk_size
|
chunk_size
|
||||||
in
|
in
|
||||||
let refill = ref true in
|
let refill = ref true in
|
||||||
|
|
@ -284,9 +275,7 @@ let read_chunked ?(buf=Buf.create()) ~fail (bs:t) : t=
|
||||||
~fill:(fun self ->
|
~fill:(fun self ->
|
||||||
(* do we need to refill? *)
|
(* do we need to refill? *)
|
||||||
if self.off >= self.len then (
|
if self.off >= self.len then (
|
||||||
if !chunk_size = 0 && !refill then (
|
if !chunk_size = 0 && !refill then chunk_size := read_next_chunk_len ();
|
||||||
chunk_size := read_next_chunk_len();
|
|
||||||
);
|
|
||||||
self.off <- 0;
|
self.off <- 0;
|
||||||
self.len <- 0;
|
self.len <- 0;
|
||||||
if !chunk_size > 0 then (
|
if !chunk_size > 0 then (
|
||||||
|
|
@ -296,36 +285,31 @@ let read_chunked ?(buf=Buf.create()) ~fail (bs:t) : t=
|
||||||
~too_short:(fun () -> raise (fail "chunk is too short"))
|
~too_short:(fun () -> raise (fail "chunk is too short"))
|
||||||
bs self.bs to_read;
|
bs self.bs to_read;
|
||||||
self.len <- to_read;
|
self.len <- to_read;
|
||||||
chunk_size := !chunk_size - to_read;
|
chunk_size := !chunk_size - to_read
|
||||||
) else (
|
) else
|
||||||
refill := false; (* stream is finished *)
|
refill := false (* stream is finished *)
|
||||||
)
|
))
|
||||||
);
|
|
||||||
)
|
|
||||||
~consume:(fun self n ->
|
~consume:(fun self n ->
|
||||||
self.off <- self.off + n;
|
self.off <- self.off + n;
|
||||||
self.len <- self.len - n)
|
self.len <- self.len - n)
|
||||||
~close:(fun self ->
|
~close:(fun self ->
|
||||||
(* close this overlay, do not close underlying stream *)
|
(* close this overlay, do not close underlying stream *)
|
||||||
self.len <- 0;
|
self.len <- 0;
|
||||||
refill:= false
|
refill := false)
|
||||||
)
|
|
||||||
()
|
()
|
||||||
|
|
||||||
(* print a stream as a series of chunks *)
|
(* print a stream as a series of chunks *)
|
||||||
let output_chunked (oc:out_channel) (self:t) : unit =
|
let output_chunked (oc : out_channel) (self : t) : unit =
|
||||||
let continue = ref true in
|
let continue = ref true in
|
||||||
while !continue do
|
while !continue do
|
||||||
(* next chunk *)
|
(* next chunk *)
|
||||||
self.fill_buf();
|
self.fill_buf ();
|
||||||
let n = self.len in
|
let n = self.len in
|
||||||
Printf.fprintf oc "%x\r\n" n;
|
Printf.fprintf oc "%x\r\n" n;
|
||||||
output oc self.bs self.off n;
|
output oc self.bs self.off n;
|
||||||
self.consume n;
|
self.consume n;
|
||||||
if n = 0 then (
|
if n = 0 then continue := false;
|
||||||
continue := false;
|
output_string oc "\r\n"
|
||||||
);
|
|
||||||
output_string oc "\r\n";
|
|
||||||
done;
|
done;
|
||||||
(* write another crlf after the stream (see #56) *)
|
(* write another crlf after the stream (see #56) *)
|
||||||
output_string oc "\r\n";
|
output_string oc "\r\n";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(** Byte streams.
|
(** Byte streams.
|
||||||
|
|
||||||
These used to live in {!Tiny_httpd} but are now in their own module.
|
These used to live in {!Tiny_httpd} but are now in their own module.
|
||||||
|
|
@ -8,31 +7,21 @@ type hidden
|
||||||
(** Type used to make {!t} unbuildable via a record literal. Use {!make} instead. *)
|
(** Type used to make {!t} unbuildable via a record literal. Use {!make} instead. *)
|
||||||
|
|
||||||
type t = {
|
type t = {
|
||||||
mutable bs: bytes;
|
mutable bs: bytes; (** The bytes *)
|
||||||
(** The bytes *)
|
mutable off: int; (** Beginning of valid slice in {!bs} *)
|
||||||
|
mutable len: int;
|
||||||
mutable off : int;
|
|
||||||
(** Beginning of valid slice in {!bs} *)
|
|
||||||
|
|
||||||
mutable len : int;
|
|
||||||
(** Length of valid slice in {!bs}. If [len = 0] after
|
(** Length of valid slice in {!bs}. If [len = 0] after
|
||||||
a call to {!fill}, then the stream is finished. *)
|
a call to {!fill}, then the stream is finished. *)
|
||||||
|
|
||||||
fill_buf: unit -> unit;
|
fill_buf: unit -> unit;
|
||||||
(** See the current slice of the internal buffer as [bytes, i, len],
|
(** See the current slice of the internal buffer as [bytes, i, len],
|
||||||
where the slice is [bytes[i] .. [bytes[i+len-1]]].
|
where the slice is [bytes[i] .. [bytes[i+len-1]]].
|
||||||
Can block to refill the buffer if there is currently no content.
|
Can block to refill the buffer if there is currently no content.
|
||||||
If [len=0] then there is no more data. *)
|
If [len=0] then there is no more data. *)
|
||||||
|
|
||||||
consume: int -> unit;
|
consume: int -> unit;
|
||||||
(** Consume [n] bytes from the buffer.
|
(** Consume [n] bytes from the buffer.
|
||||||
This should only be called with [n <= len]. *)
|
This should only be called with [n <= len]. *)
|
||||||
|
close: unit -> unit; (** Close the stream. *)
|
||||||
close: unit -> unit;
|
_rest: hidden; (** Use {!make} to build a stream. *)
|
||||||
(** Close the stream. *)
|
|
||||||
|
|
||||||
_rest: hidden;
|
|
||||||
(** Use {!make} to build a stream. *)
|
|
||||||
}
|
}
|
||||||
(** A buffered stream, with a view into the current buffer (or refill if empty),
|
(** A buffered stream, with a view into the current buffer (or refill if empty),
|
||||||
and a function to consume [n] bytes.
|
and a function to consume [n] bytes.
|
||||||
|
|
@ -75,7 +64,8 @@ val make :
|
||||||
?close:(t -> unit) ->
|
?close:(t -> unit) ->
|
||||||
consume:(t -> int -> unit) ->
|
consume:(t -> int -> unit) ->
|
||||||
fill:(t -> unit) ->
|
fill:(t -> unit) ->
|
||||||
unit -> t
|
unit ->
|
||||||
|
t
|
||||||
(** [make ~fill ()] creates a byte stream.
|
(** [make ~fill ()] creates a byte stream.
|
||||||
@param fill is used to refill the buffer, and is called initially.
|
@param fill is used to refill the buffer, and is called initially.
|
||||||
@param close optional closing.
|
@param close optional closing.
|
||||||
|
|
@ -95,18 +85,12 @@ val read_all : ?buf:Tiny_httpd_buf.t -> t -> string
|
||||||
@param buf a buffer to (re)use. Its content will be cleared. *)
|
@param buf a buffer to (re)use. Its content will be cleared. *)
|
||||||
|
|
||||||
val limit_size_to :
|
val limit_size_to :
|
||||||
close_rec:bool ->
|
close_rec:bool -> max_size:int -> too_big:(int -> unit) -> t -> t
|
||||||
max_size:int ->
|
|
||||||
too_big:(int -> unit) ->
|
|
||||||
t -> t
|
|
||||||
(* New stream with maximum size [max_size].
|
(* New stream with maximum size [max_size].
|
||||||
@param close_rec if true, closing this will also close the input stream
|
@param close_rec if true, closing this will also close the input stream
|
||||||
@param too_big called with read size if the max size is reached *)
|
@param too_big called with read size if the max size is reached *)
|
||||||
|
|
||||||
val read_chunked :
|
val read_chunked : ?buf:Tiny_httpd_buf.t -> fail:(string -> exn) -> t -> t
|
||||||
?buf:Tiny_httpd_buf.t ->
|
|
||||||
fail:(string -> exn) ->
|
|
||||||
t -> t
|
|
||||||
(** Convert a stream into a stream of byte chunks using
|
(** Convert a stream into a stream of byte chunks using
|
||||||
the chunked encoding. The size of chunks is not specified.
|
the chunked encoding. The size of chunks is not specified.
|
||||||
@param buf buffer used for intermediate storage.
|
@param buf buffer used for intermediate storage.
|
||||||
|
|
@ -114,8 +98,7 @@ val read_chunked :
|
||||||
*)
|
*)
|
||||||
|
|
||||||
val read_exactly :
|
val read_exactly :
|
||||||
close_rec:bool -> size:int -> too_short:(int -> unit) ->
|
close_rec:bool -> size:int -> too_short:(int -> unit) -> t -> t
|
||||||
t -> t
|
|
||||||
(** [read_exactly ~size bs] returns a new stream that reads exactly
|
(** [read_exactly ~size bs] returns a new stream that reads exactly
|
||||||
[size] bytes from [bs], and then closes.
|
[size] bytes from [bs], and then closes.
|
||||||
@param close_rec if true, closing the resulting stream also closes
|
@param close_rec if true, closing the resulting stream also closes
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(* test utils *)
|
(* test utils *)
|
||||||
(*$inject
|
(*$inject
|
||||||
let pp_res f = function Ok x -> f x | Error e -> e
|
let pp_res f = function Ok x -> f x | Error e -> e
|
||||||
|
|
@ -9,17 +8,15 @@
|
||||||
let is_ascii_char c = Char.code c < 128
|
let is_ascii_char c = Char.code c < 128
|
||||||
*)
|
*)
|
||||||
|
|
||||||
let percent_encode ?(skip=fun _->false) s =
|
let percent_encode ?(skip = fun _ -> false) s =
|
||||||
let buf = Buffer.create (String.length s) in
|
let buf = Buffer.create (String.length s) in
|
||||||
String.iter
|
String.iter
|
||||||
(function
|
(function
|
||||||
| c when skip c -> Buffer.add_char buf c
|
| c when skip c -> Buffer.add_char buf c
|
||||||
| (' ' | '!' | '"' | '#' | '$' | '%' | '&' | '\'' | '(' | ')' | '*' | '+'
|
| ( ' ' | '!' | '"' | '#' | '$' | '%' | '&' | '\'' | '(' | ')' | '*' | '+'
|
||||||
| ',' | '/' | ':' | ';' | '=' | '?' | '@' | '[' | ']' | '~')
|
| ',' | '/' | ':' | ';' | '=' | '?' | '@' | '[' | ']' | '~' ) as c ->
|
||||||
as c ->
|
|
||||||
Printf.bprintf buf "%%%X" (Char.code c)
|
|
||||||
| c when Char.code c > 127 ->
|
|
||||||
Printf.bprintf buf "%%%X" (Char.code c)
|
Printf.bprintf buf "%%%X" (Char.code c)
|
||||||
|
| c when Char.code c > 127 -> Printf.bprintf buf "%%%X" (Char.code c)
|
||||||
| c -> Buffer.add_char buf c)
|
| c -> Buffer.add_char buf c)
|
||||||
s;
|
s;
|
||||||
Buffer.contents buf
|
Buffer.contents buf
|
||||||
|
|
@ -34,26 +31,28 @@ let percent_encode ?(skip=fun _->false) s =
|
||||||
(Some "?") (percent_decode @@ percent_encode "?")
|
(Some "?") (percent_decode @@ percent_encode "?")
|
||||||
*)
|
*)
|
||||||
|
|
||||||
let hex_int (s:string) : int = Scanf.sscanf s "%x" (fun x->x)
|
let hex_int (s : string) : int = Scanf.sscanf s "%x" (fun x -> x)
|
||||||
|
|
||||||
let percent_decode (s:string) : _ option =
|
let percent_decode (s : string) : _ option =
|
||||||
let buf = Buffer.create (String.length s) in
|
let buf = Buffer.create (String.length s) in
|
||||||
let i = ref 0 in
|
let i = ref 0 in
|
||||||
try
|
try
|
||||||
while !i < String.length s do
|
while !i < String.length s do
|
||||||
match String.get s !i with
|
match String.get s !i with
|
||||||
| '%' ->
|
| '%' ->
|
||||||
if !i+2 < String.length s then (
|
if !i + 2 < String.length s then (
|
||||||
begin match hex_int @@ String.sub s (!i+1) 2 with
|
(match hex_int @@ String.sub s (!i + 1) 2 with
|
||||||
| n -> Buffer.add_char buf (Char.chr n)
|
| n -> Buffer.add_char buf (Char.chr n)
|
||||||
| exception _ -> raise Exit
|
| exception _ -> raise Exit);
|
||||||
end;
|
i := !i + 3
|
||||||
i := !i + 3;
|
) else
|
||||||
) else (
|
|
||||||
raise Exit (* truncated *)
|
raise Exit (* truncated *)
|
||||||
)
|
| '+' ->
|
||||||
| '+' -> Buffer.add_char buf ' '; incr i (* for query strings *)
|
Buffer.add_char buf ' ';
|
||||||
| c -> Buffer.add_char buf c; incr i
|
incr i (* for query strings *)
|
||||||
|
| c ->
|
||||||
|
Buffer.add_char buf c;
|
||||||
|
incr i
|
||||||
done;
|
done;
|
||||||
Some (Buffer.contents buf)
|
Some (Buffer.contents buf)
|
||||||
with Exit -> None
|
with Exit -> None
|
||||||
|
|
@ -77,7 +76,7 @@ let get_non_query_path s =
|
||||||
|
|
||||||
let get_query s : string =
|
let get_query s : string =
|
||||||
match find_q_index_ s with
|
match find_q_index_ s with
|
||||||
| i -> String.sub s (i+1) (String.length s-i-1)
|
| i -> String.sub s (i + 1) (String.length s - i - 1)
|
||||||
| exception Not_found -> ""
|
| exception Not_found -> ""
|
||||||
|
|
||||||
let split_query s = get_non_query_path s, get_query s
|
let split_query s = get_non_query_path s, get_query s
|
||||||
|
|
@ -89,16 +88,11 @@ let split_on_slash s : _ list =
|
||||||
while !i < n do
|
while !i < n do
|
||||||
match String.index_from s !i '/' with
|
match String.index_from s !i '/' with
|
||||||
| exception Not_found ->
|
| exception Not_found ->
|
||||||
if !i < n then (
|
if !i < n then (* last component *) l := String.sub s !i (n - !i) :: !l;
|
||||||
(* last component *)
|
|
||||||
l := String.sub s !i (n - !i) :: !l;
|
|
||||||
);
|
|
||||||
i := n (* done *)
|
i := n (* done *)
|
||||||
| j ->
|
| j ->
|
||||||
if j > !i then (
|
if j > !i then l := String.sub s !i (j - !i) :: !l;
|
||||||
l := String.sub s !i (j - !i) :: !l;
|
i := j + 1
|
||||||
);
|
|
||||||
i := j+1;
|
|
||||||
done;
|
done;
|
||||||
List.rev !l
|
List.rev !l
|
||||||
|
|
||||||
|
|
@ -112,31 +106,38 @@ let split_on_slash s : _ list =
|
||||||
[] (split_on_slash "//")
|
[] (split_on_slash "//")
|
||||||
*)
|
*)
|
||||||
|
|
||||||
let parse_query s : (_ list, string) result=
|
let parse_query s : (_ list, string) result =
|
||||||
let pairs = ref [] in
|
let pairs = ref [] in
|
||||||
let is_sep_ = function '&' | ';' -> true | _ -> false in
|
let is_sep_ = function
|
||||||
|
| '&' | ';' -> true
|
||||||
|
| _ -> false
|
||||||
|
in
|
||||||
let i = ref 0 in
|
let i = ref 0 in
|
||||||
let j = ref 0 in
|
let j = ref 0 in
|
||||||
try
|
try
|
||||||
let percent_decode s =
|
let percent_decode s =
|
||||||
match percent_decode s with Some x -> x | None -> raise Invalid_query
|
match percent_decode s with
|
||||||
|
| Some x -> x
|
||||||
|
| None -> raise Invalid_query
|
||||||
in
|
in
|
||||||
let parse_pair () =
|
let parse_pair () =
|
||||||
let eq = String.index_from s !i '=' in
|
let eq = String.index_from s !i '=' in
|
||||||
let k = percent_decode @@ String.sub s !i (eq- !i) in
|
let k = percent_decode @@ String.sub s !i (eq - !i) in
|
||||||
let v = percent_decode @@ String.sub s (eq+1) (!j-eq-1) in
|
let v = percent_decode @@ String.sub s (eq + 1) (!j - eq - 1) in
|
||||||
pairs := (k,v) :: !pairs;
|
pairs := (k, v) :: !pairs
|
||||||
in
|
in
|
||||||
while !i < String.length s do
|
while !i < String.length s do
|
||||||
while !j < String.length s && not (is_sep_ (String.get s !j)) do incr j done;
|
while !j < String.length s && not (is_sep_ (String.get s !j)) do
|
||||||
|
incr j
|
||||||
|
done;
|
||||||
if !j < String.length s then (
|
if !j < String.length s then (
|
||||||
assert (is_sep_ (String.get s !j));
|
assert (is_sep_ (String.get s !j));
|
||||||
parse_pair();
|
parse_pair ();
|
||||||
i := !j+1;
|
i := !j + 1;
|
||||||
j := !i;
|
j := !i
|
||||||
) else (
|
) else (
|
||||||
parse_pair();
|
parse_pair ();
|
||||||
i := String.length s; (* done *)
|
i := String.length s (* done *)
|
||||||
)
|
)
|
||||||
done;
|
done;
|
||||||
Ok !pairs
|
Ok !pairs
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ val get_query : string -> string
|
||||||
(** Obtain the query part of a path.
|
(** Obtain the query part of a path.
|
||||||
@since 0.4 *)
|
@since 0.4 *)
|
||||||
|
|
||||||
val parse_query : string -> ((string*string) list, string) result
|
val parse_query : string -> ((string * string) list, string) result
|
||||||
(** Parse a query as a list of ['&'] or [';'] separated [key=value] pairs.
|
(** Parse a query as a list of ['&'] or [';'] separated [key=value] pairs.
|
||||||
The order might not be preserved.
|
The order might not be preserved.
|
||||||
@since 0.3
|
@since 0.3
|
||||||
|
|
|
||||||
151
src/bin/curly.ml
151
src/bin/curly.ml
|
|
@ -1,11 +1,12 @@
|
||||||
module Result = struct
|
module Result = struct
|
||||||
include Result
|
include Result
|
||||||
let (>>=)
|
|
||||||
: type a b e. (a, e) result -> (a -> (b, e) result) -> (b, e) result
|
let ( >>= ) :
|
||||||
= fun r f ->
|
type a b e. (a, e) result -> (a -> (b, e) result) -> (b, e) result =
|
||||||
|
fun r f ->
|
||||||
match r with
|
match r with
|
||||||
| Ok x -> f x
|
| Ok x -> f x
|
||||||
| (Error _) as e -> e
|
| Error _ as e -> e
|
||||||
end
|
end
|
||||||
|
|
||||||
open Result
|
open Result
|
||||||
|
|
@ -21,8 +22,7 @@ module Meth = struct
|
||||||
| `TRACE
|
| `TRACE
|
||||||
| `CONNECT
|
| `CONNECT
|
||||||
| `PATCH
|
| `PATCH
|
||||||
| `Other of string
|
| `Other of string ]
|
||||||
]
|
|
||||||
|
|
||||||
let to_string = function
|
let to_string = function
|
||||||
| `GET -> "GET"
|
| `GET -> "GET"
|
||||||
|
|
@ -46,44 +46,31 @@ module Header = struct
|
||||||
|
|
||||||
let to_cmd t =
|
let to_cmd t =
|
||||||
t
|
t
|
||||||
|> List.map (fun (k, v) -> ["-H"; Printf.sprintf "%s: %s" k v])
|
|> List.map (fun (k, v) -> [ "-H"; Printf.sprintf "%s: %s" k v ])
|
||||||
|> List.concat
|
|> List.concat
|
||||||
|
|
||||||
let pp fmt t =
|
let pp fmt t =
|
||||||
Format.pp_print_list
|
Format.pp_print_list ~pp_sep:Format.pp_print_newline
|
||||||
~pp_sep:Format.pp_print_newline
|
(fun fmt (k, v) -> Format.fprintf fmt "%s: %s\n" k v)
|
||||||
(fun fmt (k ,v) -> Format.fprintf fmt "%s: %s\n" k v)
|
|
||||||
fmt t
|
fmt t
|
||||||
end
|
end
|
||||||
|
|
||||||
module Response = struct
|
module Response = struct
|
||||||
type t = Http.response =
|
type t = Http.response = { code: int; headers: Header.t; body: string }
|
||||||
{ code: int
|
|
||||||
; headers: Header.t
|
|
||||||
; body: string
|
|
||||||
}
|
|
||||||
|
|
||||||
let default =
|
let default = { code = 0; headers = []; body = "" }
|
||||||
{ code = 0
|
|
||||||
; headers = []
|
|
||||||
; body = "" }
|
|
||||||
|
|
||||||
let of_stdout s =
|
let of_stdout s =
|
||||||
let lexbuf = Lexing.from_string s in
|
let lexbuf = Lexing.from_string s in
|
||||||
try Ok (Http.response default lexbuf)
|
try Ok (Http.response default lexbuf) with e -> Error e
|
||||||
with e -> Error e
|
|
||||||
|
|
||||||
let pp fmt t =
|
let pp fmt t =
|
||||||
Format.fprintf fmt "{code=%d;@ headers=%a;@ body=\"%s\"}"
|
Format.fprintf fmt "{code=%d;@ headers=%a;@ body=\"%s\"}" t.code Header.pp
|
||||||
t.code Header.pp t.headers t.body
|
t.headers t.body
|
||||||
end
|
end
|
||||||
|
|
||||||
module Process_result = struct
|
module Process_result = struct
|
||||||
type t =
|
type t = { status: Unix.process_status; stderr: string; stdout: string }
|
||||||
{ status: Unix.process_status
|
|
||||||
; stderr: string
|
|
||||||
; stdout: string
|
|
||||||
}
|
|
||||||
|
|
||||||
let pp_process_status fmt = function
|
let pp_process_status fmt = function
|
||||||
| Unix.WEXITED n -> Format.fprintf fmt "Exit code %d" n
|
| Unix.WEXITED n -> Format.fprintf fmt "Exit code %d" n
|
||||||
|
|
@ -114,42 +101,35 @@ module Error = struct
|
||||||
end
|
end
|
||||||
|
|
||||||
module Request = struct
|
module Request = struct
|
||||||
type t =
|
type t = { meth: Meth.t; url: string; headers: Header.t; body: string }
|
||||||
{ meth: Meth.t
|
|
||||||
; url: string
|
|
||||||
; headers: Header.t
|
|
||||||
; body: string
|
|
||||||
}
|
|
||||||
|
|
||||||
let make ?(headers=Header.empty) ?(body="") ~url ~meth () =
|
let make ?(headers = Header.empty) ?(body = "") ~url ~meth () =
|
||||||
{ meth
|
{ meth; url; headers; body }
|
||||||
; url
|
|
||||||
; headers
|
|
||||||
; body }
|
|
||||||
|
|
||||||
let has_body t = String.length t.body > 0
|
let has_body t = String.length t.body > 0
|
||||||
|
|
||||||
let validate t =
|
let validate t =
|
||||||
if has_body t && List.mem t.meth [`GET; `HEAD] then
|
if has_body t && List.mem t.meth [ `GET; `HEAD ] then
|
||||||
Error (Error.Invalid_request "No body is allowed with GET/HEAD methods")
|
Error (Error.Invalid_request "No body is allowed with GET/HEAD methods")
|
||||||
else
|
else
|
||||||
Ok t
|
Ok t
|
||||||
|
|
||||||
let to_cmd_args t =
|
let to_cmd_args t =
|
||||||
List.concat
|
List.concat
|
||||||
[ ["-X"; Meth.to_string t.meth]
|
[
|
||||||
; Header.to_cmd t.headers
|
[ "-X"; Meth.to_string t.meth ];
|
||||||
; [t.url]
|
Header.to_cmd t.headers;
|
||||||
; (if has_body t then
|
[ t.url ];
|
||||||
["--data-binary"; "@-"]
|
(if has_body t then
|
||||||
|
[ "--data-binary"; "@-" ]
|
||||||
else
|
else
|
||||||
[])
|
[]);
|
||||||
]
|
]
|
||||||
|
|
||||||
let pp fmt t =
|
let pp fmt t =
|
||||||
Format.fprintf fmt
|
Format.fprintf fmt
|
||||||
"{@ meth=%a;@ url=\"%s\";@ headers=\"%a\";@ body=\"%s\"@ }"
|
"{@ meth=%a;@ url=\"%s\";@ headers=\"%a\";@ body=\"%s\"@ }" Meth.pp t.meth
|
||||||
Meth.pp t.meth t.url Header.pp t.headers t.body
|
t.url Header.pp t.headers t.body
|
||||||
end
|
end
|
||||||
|
|
||||||
let result_of_process_result t =
|
let result_of_process_result t =
|
||||||
|
|
@ -158,67 +138,58 @@ let result_of_process_result t =
|
||||||
| _ -> Error (Error.Bad_exit t)
|
| _ -> Error (Error.Bad_exit t)
|
||||||
|
|
||||||
let run prog args stdin_str =
|
let run prog args stdin_str =
|
||||||
let (stdout, stdin, stderr) =
|
let stdout, stdin, stderr =
|
||||||
let prog =
|
let prog = prog :: List.map Filename.quote args |> String.concat " " in
|
||||||
prog :: (List.map Filename.quote args)
|
Unix.open_process_full prog [||]
|
||||||
|> String.concat " " in
|
in
|
||||||
Unix.open_process_full prog [||] in
|
if String.length stdin_str > 0 then output_string stdin stdin_str;
|
||||||
if String.length stdin_str > 0 then (
|
(try close_out stdin with _ -> ());
|
||||||
output_string stdin stdin_str
|
|
||||||
);
|
|
||||||
begin
|
|
||||||
try close_out stdin;
|
|
||||||
with _ -> ()
|
|
||||||
end;
|
|
||||||
let stdout_fd = Unix.descr_of_in_channel stdout in
|
let stdout_fd = Unix.descr_of_in_channel stdout in
|
||||||
let stderr_fd = Unix.descr_of_in_channel stderr in
|
let stderr_fd = Unix.descr_of_in_channel stderr in
|
||||||
let (in_buf, err_buf) = Buffer.(create 128, create 128) in
|
let in_buf, err_buf = Buffer.(create 128, create 128) in
|
||||||
let read_buf_len = 512 in
|
let read_buf_len = 512 in
|
||||||
let read_buf = Bytes.create read_buf_len in
|
let read_buf = Bytes.create read_buf_len in
|
||||||
let input ch =
|
let input ch =
|
||||||
match input ch read_buf 0 read_buf_len with
|
match input ch read_buf 0 read_buf_len with
|
||||||
| 0 -> Error `Eof
|
| 0 -> Error `Eof
|
||||||
| s -> Ok s in
|
| s -> Ok s
|
||||||
|
in
|
||||||
let rec loop = function
|
let rec loop = function
|
||||||
| [] -> ()
|
| [] -> ()
|
||||||
| read_list ->
|
| read_list ->
|
||||||
let can_read, _, _ = Unix.select read_list [] [] 1.0 in
|
let can_read, _, _ = Unix.select read_list [] [] 1.0 in
|
||||||
let to_remove =
|
let to_remove =
|
||||||
List.fold_left (fun to_remove fh ->
|
List.fold_left
|
||||||
let (rr, buf) =
|
(fun to_remove fh ->
|
||||||
if fh = stderr_fd then (
|
let rr, buf =
|
||||||
(input stderr, err_buf)
|
if fh = stderr_fd then
|
||||||
) else (
|
input stderr, err_buf
|
||||||
(input stdout, in_buf)
|
else
|
||||||
) in
|
input stdout, in_buf
|
||||||
begin match rr with
|
in
|
||||||
|
match rr with
|
||||||
| Ok len ->
|
| Ok len ->
|
||||||
Buffer.add_subbytes buf read_buf 0 len;
|
Buffer.add_subbytes buf read_buf 0 len;
|
||||||
to_remove
|
to_remove
|
||||||
| Error `Eof ->
|
| Error `Eof -> fh :: to_remove)
|
||||||
fh :: to_remove
|
[] can_read
|
||||||
end
|
|
||||||
) [] can_read in
|
|
||||||
read_list
|
|
||||||
|> List.filter (fun fh -> not (List.mem fh to_remove))
|
|
||||||
|> loop
|
|
||||||
in
|
in
|
||||||
ignore (loop [ stdout_fd ; stderr_fd ]);
|
read_list |> List.filter (fun fh -> not (List.mem fh to_remove)) |> loop
|
||||||
|
in
|
||||||
|
ignore (loop [ stdout_fd; stderr_fd ]);
|
||||||
let status = Unix.close_process_full (stdout, stdin, stderr) in
|
let status = Unix.close_process_full (stdout, stdin, stderr) in
|
||||||
{ Process_result.
|
{
|
||||||
status
|
Process_result.status;
|
||||||
; stdout = Buffer.contents in_buf
|
stdout = Buffer.contents in_buf;
|
||||||
; stderr = Buffer.contents err_buf
|
stderr = Buffer.contents err_buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
let run ?(exe="curl") ?(args=[]) req =
|
let run ?(exe = "curl") ?(args = []) req =
|
||||||
Request.validate req >>= fun req ->
|
Request.validate req >>= fun req ->
|
||||||
let args = "-si" :: (Request.to_cmd_args req) @ args in
|
let args = ("-si" :: Request.to_cmd_args req) @ args in
|
||||||
let res =
|
let res =
|
||||||
try
|
try result_of_process_result (run exe args req.Request.body)
|
||||||
result_of_process_result (run exe args req.Request.body)
|
with e -> Error (Error.Exn e)
|
||||||
with e ->
|
|
||||||
Error (Error.Exn e)
|
|
||||||
in
|
in
|
||||||
res >>= fun res ->
|
res >>= fun res ->
|
||||||
match Response.of_stdout res.Process_result.stdout with
|
match Response.of_stdout res.Process_result.stdout with
|
||||||
|
|
@ -227,11 +198,15 @@ let run ?(exe="curl") ?(args=[]) req =
|
||||||
|
|
||||||
let get ?exe ?args ?headers url =
|
let get ?exe ?args ?headers url =
|
||||||
run ?exe ?args (Request.make ?headers ~url ~meth:`GET ())
|
run ?exe ?args (Request.make ?headers ~url ~meth:`GET ())
|
||||||
|
|
||||||
let head ?exe ?args ?headers url =
|
let head ?exe ?args ?headers url =
|
||||||
run ?exe ?args (Request.make ?headers ~url ~meth:`HEAD ())
|
run ?exe ?args (Request.make ?headers ~url ~meth:`HEAD ())
|
||||||
|
|
||||||
let delete ?exe ?args ?headers url =
|
let delete ?exe ?args ?headers url =
|
||||||
run ?exe ?args (Request.make ?headers ~url ~meth:`DELETE ())
|
run ?exe ?args (Request.make ?headers ~url ~meth:`DELETE ())
|
||||||
|
|
||||||
let post ?exe ?args ?headers ?body url =
|
let post ?exe ?args ?headers ?body url =
|
||||||
run ?exe ?args (Request.make ?body ?headers ~url ~meth:`POST ())
|
run ?exe ?args (Request.make ?body ?headers ~url ~meth:`POST ())
|
||||||
|
|
||||||
let put ?exe ?args ?headers ?body url =
|
let put ?exe ?args ?headers ?body url =
|
||||||
run ?exe ?args (Request.make ?body ?headers ~url ~meth:`PUT ())
|
run ?exe ?args (Request.make ?body ?headers ~url ~meth:`PUT ())
|
||||||
|
|
|
||||||
|
|
@ -21,42 +21,23 @@ module Header : sig
|
||||||
end
|
end
|
||||||
|
|
||||||
module Response : sig
|
module Response : sig
|
||||||
type t =
|
type t = { code: int; headers: Header.t; body: string }
|
||||||
{ code: int
|
|
||||||
; headers: Header.t
|
|
||||||
; body:string
|
|
||||||
}
|
|
||||||
|
|
||||||
val pp : Format.formatter -> t -> unit
|
val pp : Format.formatter -> t -> unit
|
||||||
end
|
end
|
||||||
|
|
||||||
module Request : sig
|
module Request : sig
|
||||||
type t =
|
type t = { meth: Meth.t; url: string; headers: Header.t; body: string }
|
||||||
{ meth: Meth.t
|
|
||||||
; url:string
|
|
||||||
; headers: Header.t
|
|
||||||
; body:string
|
|
||||||
}
|
|
||||||
|
|
||||||
val make
|
val make :
|
||||||
: ?headers:Header.t
|
?headers:Header.t -> ?body:string -> url:string -> meth:Meth.t -> unit -> t
|
||||||
-> ?body:string
|
|
||||||
-> url:string
|
|
||||||
-> meth:Meth.t
|
|
||||||
-> unit
|
|
||||||
-> t
|
|
||||||
|
|
||||||
val to_cmd_args : t -> string list
|
val to_cmd_args : t -> string list
|
||||||
|
|
||||||
val pp : Format.formatter -> t -> unit
|
val pp : Format.formatter -> t -> unit
|
||||||
end
|
end
|
||||||
|
|
||||||
module Process_result : sig
|
module Process_result : sig
|
||||||
type t =
|
type t = { status: Unix.process_status; stderr: string; stdout: string }
|
||||||
{ status: Unix.process_status
|
|
||||||
; stderr:string
|
|
||||||
; stdout:string
|
|
||||||
}
|
|
||||||
|
|
||||||
val pp : Format.formatter -> t -> unit
|
val pp : Format.formatter -> t -> unit
|
||||||
end
|
end
|
||||||
|
|
@ -71,55 +52,55 @@ module Error : sig
|
||||||
val pp : Format.formatter -> t -> unit
|
val pp : Format.formatter -> t -> unit
|
||||||
end
|
end
|
||||||
|
|
||||||
val run
|
val run :
|
||||||
: ?exe:string
|
?exe:string ->
|
||||||
-> ?args:string list
|
?args:string list ->
|
||||||
-> Request.t
|
Request.t ->
|
||||||
-> (Response.t, Error.t) Result.result
|
(Response.t, Error.t) Result.result
|
||||||
|
|
||||||
val get
|
val get :
|
||||||
: ?exe:string
|
?exe:string ->
|
||||||
-> ?args:string list
|
?args:string list ->
|
||||||
-> ?headers:Header.t
|
?headers:Header.t ->
|
||||||
-> string
|
string ->
|
||||||
-> (Response.t, Error.t) Result.result
|
(Response.t, Error.t) Result.result
|
||||||
(** Specialized version of {!run} for method [`GET]
|
(** Specialized version of {!run} for method [`GET]
|
||||||
@since 0.2.0 *)
|
@since 0.2.0 *)
|
||||||
|
|
||||||
val head
|
val head :
|
||||||
: ?exe:string
|
?exe:string ->
|
||||||
-> ?args:string list
|
?args:string list ->
|
||||||
-> ?headers:Header.t
|
?headers:Header.t ->
|
||||||
-> string
|
string ->
|
||||||
-> (Response.t, Error.t) Result.result
|
(Response.t, Error.t) Result.result
|
||||||
(** Specialized version of {!run} for method [`HEAD]
|
(** Specialized version of {!run} for method [`HEAD]
|
||||||
@since 0.2.0 *)
|
@since 0.2.0 *)
|
||||||
|
|
||||||
val delete
|
val delete :
|
||||||
: ?exe:string
|
?exe:string ->
|
||||||
-> ?args:string list
|
?args:string list ->
|
||||||
-> ?headers:Header.t
|
?headers:Header.t ->
|
||||||
-> string
|
string ->
|
||||||
-> (Response.t, Error.t) Result.result
|
(Response.t, Error.t) Result.result
|
||||||
(** Specialized version of {!run} for method [`DELETE]
|
(** Specialized version of {!run} for method [`DELETE]
|
||||||
@since 0.2.0 *)
|
@since 0.2.0 *)
|
||||||
|
|
||||||
val post
|
val post :
|
||||||
: ?exe:string
|
?exe:string ->
|
||||||
-> ?args:string list
|
?args:string list ->
|
||||||
-> ?headers:Header.t
|
?headers:Header.t ->
|
||||||
-> ?body:string
|
?body:string ->
|
||||||
-> string
|
string ->
|
||||||
-> (Response.t, Error.t) Result.result
|
(Response.t, Error.t) Result.result
|
||||||
(** Specialized version of {!run} for method [`POST]
|
(** Specialized version of {!run} for method [`POST]
|
||||||
@since 0.2.0 *)
|
@since 0.2.0 *)
|
||||||
|
|
||||||
val put
|
val put :
|
||||||
: ?exe:string
|
?exe:string ->
|
||||||
-> ?args:string list
|
?args:string list ->
|
||||||
-> ?headers:Header.t
|
?headers:Header.t ->
|
||||||
-> ?body:string
|
?body:string ->
|
||||||
-> string
|
string ->
|
||||||
-> (Response.t, Error.t) Result.result
|
(Response.t, Error.t) Result.result
|
||||||
(** Specialized version of {!run} for method [`PUT]
|
(** Specialized version of {!run} for method [`PUT]
|
||||||
@since 0.2.0 *)
|
@since 0.2.0 *)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(executable
|
(executable
|
||||||
(name http_of_dir)
|
(name http_of_dir)
|
||||||
(public_name http_of_dir)
|
(public_name http_of_dir)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
(* The purpose of this module isn't to be a full blown http parser but rather to
|
(* The purpose of this module isn't to be a full blown http parser but rather to
|
||||||
only parse whatever curl otputs *)
|
only parse whatever curl otputs *)
|
||||||
type response =
|
type response = { code: int; headers: (string * string) list; body: string }
|
||||||
{ code: int
|
|
||||||
; headers: (string * string) list
|
|
||||||
; body: string
|
|
||||||
}
|
|
||||||
|
|
||||||
val response : response -> Lexing.lexbuf -> response
|
val response : response -> Lexing.lexbuf -> response
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ module U = Tiny_httpd_util
|
||||||
module D = Tiny_httpd_dir
|
module D = Tiny_httpd_dir
|
||||||
module Pf = Printf
|
module Pf = Printf
|
||||||
|
|
||||||
let serve ~config (dir:string) addr port j : _ result =
|
let serve ~config (dir : string) addr port j : _ result =
|
||||||
let server = S.create ~max_connections:j ~addr ~port () in
|
let server = S.create ~max_connections:j ~addr ~port () in
|
||||||
Printf.printf "serve directory %s on http://%(%s%):%d\n%!"
|
Printf.printf "serve directory %s on http://%(%s%):%d\n%!" dir
|
||||||
dir (if S.is_ipv6 server then "[%s]" else "%s") addr port;
|
(if S.is_ipv6 server then
|
||||||
|
"[%s]"
|
||||||
|
else
|
||||||
|
"%s")
|
||||||
|
addr port;
|
||||||
|
|
||||||
D.add_dir_path ~config ~dir ~prefix:"" server;
|
D.add_dir_path ~config ~dir ~prefix:"" server;
|
||||||
S.run server
|
S.run server
|
||||||
|
|
@ -14,43 +18,62 @@ let serve ~config (dir:string) addr port j : _ result =
|
||||||
let parse_size s : int =
|
let parse_size s : int =
|
||||||
try Scanf.sscanf s "%dM" (fun n -> n * 1_024 * 1_024)
|
try Scanf.sscanf s "%dM" (fun n -> n * 1_024 * 1_024)
|
||||||
with _ ->
|
with _ ->
|
||||||
try Scanf.sscanf s "%dk" (fun n -> n * 1_024)
|
(try Scanf.sscanf s "%dk" (fun n -> n * 1_024)
|
||||||
with _ ->
|
with _ ->
|
||||||
try int_of_string s
|
(try int_of_string s
|
||||||
with _ -> raise (Arg.Bad "invalid size (expected <int>[kM]?)")
|
with _ -> raise (Arg.Bad "invalid size (expected <int>[kM]?)")))
|
||||||
|
|
||||||
let main () =
|
let main () =
|
||||||
let config =
|
let config = D.config ~dir_behavior:Index_or_lists () in
|
||||||
D.config ~dir_behavior:Index_or_lists ()
|
|
||||||
in
|
|
||||||
let dir_ = ref "." in
|
let dir_ = ref "." in
|
||||||
let addr = ref "127.0.0.1" in
|
let addr = ref "127.0.0.1" in
|
||||||
let port = ref 8080 in
|
let port = ref 8080 in
|
||||||
let j = ref 32 in
|
let j = ref 32 in
|
||||||
Arg.parse (Arg.align [
|
Arg.parse
|
||||||
|
(Arg.align
|
||||||
|
[
|
||||||
"--addr", Set_string addr, " address to listen on";
|
"--addr", Set_string addr, " address to listen on";
|
||||||
"-a", Set_string addr, " alias to --listen";
|
"-a", Set_string addr, " alias to --listen";
|
||||||
"--port", Set_int port, " port to listen on";
|
"--port", Set_int port, " port to listen on";
|
||||||
"-p", Set_int port, " alias to --port";
|
"-p", Set_int port, " alias to --port";
|
||||||
"--dir", Set_string dir_, " directory to serve (default: \".\")";
|
"--dir", Set_string dir_, " directory to serve (default: \".\")";
|
||||||
"--debug", Unit (fun () -> S._enable_debug true), " debug mode";
|
"--debug", Unit (fun () -> S._enable_debug true), " debug mode";
|
||||||
"--upload", Unit (fun () -> config.upload <- true), " enable file uploading";
|
( "--upload",
|
||||||
"--no-upload", Unit (fun () -> config.upload <- false), " disable file uploading";
|
Unit (fun () -> config.upload <- true),
|
||||||
"--download", Unit (fun () -> config.download <- true), " enable file downloading";
|
" enable file uploading" );
|
||||||
"--no-download", Unit (fun () -> config.download <- false), " disable file downloading";
|
( "--no-upload",
|
||||||
"--max-upload", String (fun i -> config.max_upload_size <- parse_size i),
|
Unit (fun () -> config.upload <- false),
|
||||||
" maximum size of files that can be uploaded";
|
" disable file uploading" );
|
||||||
"--auto-index",
|
( "--download",
|
||||||
Bool (fun b -> config.dir_behavior <-
|
Unit (fun () -> config.download <- true),
|
||||||
(if b then Index_or_lists else Lists)),
|
" enable file downloading" );
|
||||||
" <bool> automatically redirect to index.html if present";
|
( "--no-download",
|
||||||
"--delete", Unit (fun () -> config.delete <- true), " enable `delete` on files";
|
Unit (fun () -> config.download <- false),
|
||||||
"--no-delete", Unit (fun () -> config.delete <- false), " disable `delete` on files";
|
" disable file downloading" );
|
||||||
|
( "--max-upload",
|
||||||
|
String (fun i -> config.max_upload_size <- parse_size i),
|
||||||
|
" maximum size of files that can be uploaded" );
|
||||||
|
( "--auto-index",
|
||||||
|
Bool
|
||||||
|
(fun b ->
|
||||||
|
config.dir_behavior <-
|
||||||
|
(if b then
|
||||||
|
Index_or_lists
|
||||||
|
else
|
||||||
|
Lists)),
|
||||||
|
" <bool> automatically redirect to index.html if present" );
|
||||||
|
( "--delete",
|
||||||
|
Unit (fun () -> config.delete <- true),
|
||||||
|
" enable `delete` on files" );
|
||||||
|
( "--no-delete",
|
||||||
|
Unit (fun () -> config.delete <- false),
|
||||||
|
" disable `delete` on files" );
|
||||||
"-j", Set_int j, " maximum number of simultaneous connections";
|
"-j", Set_int j, " maximum number of simultaneous connections";
|
||||||
]) (fun s -> dir_ := s) "http_of_dir [options] [dir]";
|
])
|
||||||
|
(fun s -> dir_ := s)
|
||||||
|
"http_of_dir [options] [dir]";
|
||||||
match serve ~config !dir_ !addr !port !j with
|
match serve ~config !dir_ !addr !port !j with
|
||||||
| Ok () -> ()
|
| Ok () -> ()
|
||||||
| Error e ->
|
| Error e -> raise e
|
||||||
raise e
|
|
||||||
|
|
||||||
let () = main ()
|
let () = main ()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
let spf = Printf.sprintf
|
let spf = Printf.sprintf
|
||||||
let fpf = Printf.fprintf
|
let fpf = Printf.fprintf
|
||||||
let now_ = Unix.gettimeofday()
|
let now_ = Unix.gettimeofday ()
|
||||||
let verbose = ref false
|
let verbose = ref false
|
||||||
|
|
||||||
type entry =
|
type entry =
|
||||||
|
|
@ -15,83 +14,85 @@ let read_file filename =
|
||||||
let buf = Buffer.create 32 in
|
let buf = Buffer.create 32 in
|
||||||
let b = Bytes.create 1024 in
|
let b = Bytes.create 1024 in
|
||||||
while
|
while
|
||||||
let n=input ic b 0 (Bytes.length b) in
|
let n = input ic b 0 (Bytes.length b) in
|
||||||
Buffer.add_subbytes buf b 0 n;
|
Buffer.add_subbytes buf b 0 n;
|
||||||
n > 0
|
n > 0
|
||||||
do () done;
|
do
|
||||||
|
()
|
||||||
|
done;
|
||||||
close_in ic;
|
close_in ic;
|
||||||
Buffer.contents buf
|
Buffer.contents buf
|
||||||
|
|
||||||
let split_comma s = Scanf.sscanf s "%s@,%s" (fun x y -> x,y)
|
let split_comma s = Scanf.sscanf s "%s@,%s" (fun x y -> x, y)
|
||||||
|
|
||||||
let is_url s =
|
let is_url s =
|
||||||
let is_prefix pre s =
|
let is_prefix pre s =
|
||||||
String.length s > String.length pre &&
|
String.length s > String.length pre
|
||||||
String.sub s 0 (String.length pre) = pre
|
&& String.sub s 0 (String.length pre) = pre
|
||||||
in
|
in
|
||||||
is_prefix "http://" s || is_prefix "https://" s
|
is_prefix "http://" s || is_prefix "https://" s
|
||||||
|
|
||||||
let emit oc (l:entry list) : unit =
|
let emit oc (l : entry list) : unit =
|
||||||
fpf oc "let embedded_fs = Tiny_httpd_dir.Embedded_fs.create ~mtime:%f ()\n" now_;
|
fpf oc "let embedded_fs = Tiny_httpd_dir.Embedded_fs.create ~mtime:%f ()\n"
|
||||||
|
now_;
|
||||||
|
|
||||||
let add_vfs ~mtime vfs_path content =
|
let add_vfs ~mtime vfs_path content =
|
||||||
fpf oc
|
fpf oc
|
||||||
"let () = Tiny_httpd_dir.Embedded_fs.add_file embedded_fs \n \
|
"let () = Tiny_httpd_dir.Embedded_fs.add_file embedded_fs \n\
|
||||||
~mtime:%h ~path:%S\n \
|
\ ~mtime:%h ~path:%S\n\
|
||||||
%S\n"
|
\ %S\n"
|
||||||
mtime vfs_path content
|
mtime vfs_path content
|
||||||
in
|
in
|
||||||
|
|
||||||
let rec add_entry = function
|
let rec add_entry = function
|
||||||
| File (vfs_path, actual_path) ->
|
| File (vfs_path, actual_path) ->
|
||||||
if !verbose then Printf.eprintf "add file %S = %S\n%!" vfs_path actual_path;
|
if !verbose then
|
||||||
|
Printf.eprintf "add file %S = %S\n%!" vfs_path actual_path;
|
||||||
|
|
||||||
let content = read_file actual_path in
|
let content = read_file actual_path in
|
||||||
let mtime = (Unix.stat actual_path).Unix.st_mtime in
|
let mtime = (Unix.stat actual_path).Unix.st_mtime in
|
||||||
add_vfs ~mtime vfs_path content
|
add_vfs ~mtime vfs_path content
|
||||||
|
|
||||||
| Url (vfs_path, url) ->
|
| Url (vfs_path, url) ->
|
||||||
if !verbose then Printf.eprintf "add url %S = %S\n%!" vfs_path url;
|
if !verbose then Printf.eprintf "add url %S = %S\n%!" vfs_path url;
|
||||||
|
|
||||||
begin match Curly.get ~args:["-L"] url with
|
(match Curly.get ~args:[ "-L" ] url with
|
||||||
| Ok b ->
|
| Ok b ->
|
||||||
let code = b.Curly.Response.code in
|
let code = b.Curly.Response.code in
|
||||||
if code >= 200 && code < 300 then (
|
if code >= 200 && code < 300 then
|
||||||
add_vfs ~mtime:now_ vfs_path b.Curly.Response.body
|
add_vfs ~mtime:now_ vfs_path b.Curly.Response.body
|
||||||
) else (
|
else
|
||||||
failwith (Printf.sprintf "download of %S failed with code: %d" url code)
|
failwith
|
||||||
)
|
(Printf.sprintf "download of %S failed with code: %d" url code)
|
||||||
| Error err ->
|
| Error err ->
|
||||||
failwith (Format.asprintf "download of %S failed: %a" url Curly.Error.pp err)
|
failwith
|
||||||
end
|
(Format.asprintf "download of %S failed: %a" url Curly.Error.pp err))
|
||||||
|
|
||||||
| Mirror (vfs_path, dir) ->
|
| Mirror (vfs_path, dir) ->
|
||||||
if !verbose then Printf.eprintf "mirror directory %S as %S\n%!" dir vfs_path;
|
if !verbose then
|
||||||
|
Printf.eprintf "mirror directory %S as %S\n%!" dir vfs_path;
|
||||||
|
|
||||||
let rec traverse rpath =
|
let rec traverse rpath =
|
||||||
let real_path = Filename.concat dir rpath in
|
let real_path = Filename.concat dir rpath in
|
||||||
if Sys.is_directory real_path then (
|
if Sys.is_directory real_path then (
|
||||||
let arr = Sys.readdir real_path in
|
let arr = Sys.readdir real_path in
|
||||||
Array.iter (fun e -> traverse (Filename.concat rpath e)) arr
|
Array.iter (fun e -> traverse (Filename.concat rpath e)) arr
|
||||||
) else (
|
) else
|
||||||
add_entry (File (Filename.concat vfs_path rpath, real_path))
|
add_entry (File (Filename.concat vfs_path rpath, real_path))
|
||||||
)
|
|
||||||
in
|
in
|
||||||
traverse "."
|
traverse "."
|
||||||
|
|
||||||
| Source_file f ->
|
| Source_file f ->
|
||||||
if !verbose then Printf.eprintf "read source file %S\n%!" f;
|
if !verbose then Printf.eprintf "read source file %S\n%!" f;
|
||||||
|
|
||||||
let lines =
|
let lines =
|
||||||
read_file f |> String.split_on_char '\n'
|
read_file f |> String.split_on_char '\n' |> List.map String.trim
|
||||||
|> List.map String.trim
|
|> List.filter (( <> ) "")
|
||||||
|> List.filter ((<>) "")
|
|
||||||
in
|
in
|
||||||
|
|
||||||
let process_line line =
|
let process_line line =
|
||||||
let vfs_path, path = split_comma line in
|
let vfs_path, path = split_comma line in
|
||||||
if is_url path then add_entry (Url(vfs_path, path))
|
if is_url path then
|
||||||
else add_entry (File (vfs_path, path))
|
add_entry (Url (vfs_path, path))
|
||||||
|
else
|
||||||
|
add_entry (File (vfs_path, path))
|
||||||
in
|
in
|
||||||
|
|
||||||
List.iter process_line lines
|
List.iter process_line lines
|
||||||
|
|
@ -101,8 +102,8 @@ let emit oc (l:entry list) : unit =
|
||||||
fpf oc "let vfs = Tiny_httpd_dir.Embedded_fs.to_vfs embedded_fs\n";
|
fpf oc "let vfs = Tiny_httpd_dir.Embedded_fs.to_vfs embedded_fs\n";
|
||||||
()
|
()
|
||||||
|
|
||||||
|
let help =
|
||||||
let help = {|vfs-pack [opt]+
|
{|vfs-pack [opt]+
|
||||||
|
|
||||||
Builds an OCaml module containing a `Tiny_httpd_dir.Embedded_fs.t`
|
Builds an OCaml module containing a `Tiny_httpd_dir.Embedded_fs.t`
|
||||||
virtual file system. This is useful to pack assets into an OCaml binary,
|
virtual file system. This is useful to pack assets into an OCaml binary,
|
||||||
|
|
@ -121,7 +122,6 @@ and is processed as previously. If actual_path looks like an http(s) URL
|
||||||
it is treated as such.
|
it is treated as such.
|
||||||
|}
|
|}
|
||||||
|
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
let entries = ref [] in
|
let entries = ref [] in
|
||||||
let out = ref "" in
|
let out = ref "" in
|
||||||
|
|
@ -133,30 +133,45 @@ let () =
|
||||||
add_entry (File (vfs_path, path))
|
add_entry (File (vfs_path, path))
|
||||||
and add_mirror s =
|
and add_mirror s =
|
||||||
let vfs_path, path = split_comma s in
|
let vfs_path, path = split_comma s in
|
||||||
let vfs_path, path = if path="" then "", vfs_path else vfs_path, path in
|
let vfs_path, path =
|
||||||
|
if path = "" then
|
||||||
|
"", vfs_path
|
||||||
|
else
|
||||||
|
vfs_path, path
|
||||||
|
in
|
||||||
add_entry (Mirror (vfs_path, path))
|
add_entry (Mirror (vfs_path, path))
|
||||||
and add_source f = add_entry (Source_file f)
|
and add_source f = add_entry (Source_file f)
|
||||||
and add_url s =
|
and add_url s =
|
||||||
let vfs_path, path = split_comma s in
|
let vfs_path, path = split_comma s in
|
||||||
if is_url path then add_entry (Url(vfs_path, path))
|
if is_url path then
|
||||||
else invalid_arg (spf "--url: invalid URL %S" path)
|
add_entry (Url (vfs_path, path))
|
||||||
|
else
|
||||||
|
invalid_arg (spf "--url: invalid URL %S" path)
|
||||||
in
|
in
|
||||||
|
|
||||||
let opts = [
|
let opts =
|
||||||
|
[
|
||||||
"-v", Arg.Set verbose, " verbose mode";
|
"-v", Arg.Set verbose, " verbose mode";
|
||||||
"-o", Arg.Set_string out, " set output file";
|
"-o", Arg.Set_string out, " set output file";
|
||||||
"--file", Arg.String add_file, " <name,file> adds name=file to the VFS";
|
"--file", Arg.String add_file, " <name,file> adds name=file to the VFS";
|
||||||
"--url", Arg.String add_url, " <name,url> adds name=url to the VFS";
|
"--url", Arg.String add_url, " <name,url> adds name=url to the VFS";
|
||||||
"--mirror", Arg.String add_mirror, " <prefix,dir> copies directory dir into the VFS under prefix";
|
( "--mirror",
|
||||||
"-F", Arg.String add_source, " <file> reads entries from the file, on per line";
|
Arg.String add_mirror,
|
||||||
] |> Arg.align in
|
" <prefix,dir> copies directory dir into the VFS under prefix" );
|
||||||
|
( "-F",
|
||||||
|
Arg.String add_source,
|
||||||
|
" <file> reads entries from the file, on per line" );
|
||||||
|
]
|
||||||
|
|> Arg.align
|
||||||
|
in
|
||||||
Arg.parse opts (fun _ -> raise (Arg.Help "no positional arg")) help;
|
Arg.parse opts (fun _ -> raise (Arg.Help "no positional arg")) help;
|
||||||
|
|
||||||
let out, close =
|
let out, close =
|
||||||
if !out="" then stdout,ignore
|
if !out = "" then
|
||||||
else open_out !out, close_out
|
stdout, ignore
|
||||||
|
else
|
||||||
|
open_out !out, close_out
|
||||||
in
|
in
|
||||||
emit out !entries;
|
emit out !entries;
|
||||||
close out;
|
close out;
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,149 +1,134 @@
|
||||||
|
|
||||||
module S = Tiny_httpd_server
|
module S = Tiny_httpd_server
|
||||||
module BS = Tiny_httpd_stream
|
module BS = Tiny_httpd_stream
|
||||||
|
|
||||||
let decode_deflate_stream_ ~buf_size (is:S.byte_stream) : S.byte_stream =
|
let decode_deflate_stream_ ~buf_size (is : S.byte_stream) : S.byte_stream =
|
||||||
S._debug (fun k->k "wrap stream with deflate.decode");
|
S._debug (fun k -> k "wrap stream with deflate.decode");
|
||||||
let zlib_str = Zlib.inflate_init false in
|
let zlib_str = Zlib.inflate_init false in
|
||||||
let is_done = ref false in
|
let is_done = ref false in
|
||||||
BS.make
|
BS.make ~bs:(Bytes.create buf_size)
|
||||||
~bs:(Bytes.create buf_size)
|
|
||||||
~close:(fun _ ->
|
~close:(fun _ ->
|
||||||
Zlib.inflate_end zlib_str;
|
Zlib.inflate_end zlib_str;
|
||||||
BS.close is
|
BS.close is)
|
||||||
)
|
|
||||||
~consume:(fun self len ->
|
~consume:(fun self len ->
|
||||||
if len > self.len then (
|
if len > self.len then
|
||||||
S.Response.fail_raise ~code:400
|
S.Response.fail_raise ~code:400
|
||||||
"inflate: error during decompression: invalid consume len %d (max %d)"
|
"inflate: error during decompression: invalid consume len %d (max %d)"
|
||||||
len self.len
|
len self.len;
|
||||||
);
|
|
||||||
self.off <- self.off + len;
|
self.off <- self.off + len;
|
||||||
self.len <- self.len - len;
|
self.len <- self.len - len)
|
||||||
)
|
|
||||||
~fill:(fun self ->
|
~fill:(fun self ->
|
||||||
(* refill [buf] if needed *)
|
(* refill [buf] if needed *)
|
||||||
if self.len = 0 && not !is_done then (
|
if self.len = 0 && not !is_done then (
|
||||||
is.fill_buf();
|
is.fill_buf ();
|
||||||
begin
|
(try
|
||||||
try
|
|
||||||
let finished, used_in, used_out =
|
let finished, used_in, used_out =
|
||||||
Zlib.inflate zlib_str
|
Zlib.inflate zlib_str self.bs 0 (Bytes.length self.bs) is.bs is.off
|
||||||
self.bs 0 (Bytes.length self.bs)
|
is.len Zlib.Z_SYNC_FLUSH
|
||||||
is.bs is.off is.len Zlib.Z_SYNC_FLUSH
|
|
||||||
in
|
in
|
||||||
is.consume used_in;
|
is.consume used_in;
|
||||||
self.off <- 0;
|
self.off <- 0;
|
||||||
self.len <- used_out;
|
self.len <- used_out;
|
||||||
if finished then is_done := true;
|
if finished then is_done := true;
|
||||||
S._debug (fun k->k "decode %d bytes as %d bytes from inflate (finished: %b)"
|
S._debug (fun k ->
|
||||||
used_in used_out finished);
|
k "decode %d bytes as %d bytes from inflate (finished: %b)"
|
||||||
with Zlib.Error (e1,e2) ->
|
used_in used_out finished)
|
||||||
|
with Zlib.Error (e1, e2) ->
|
||||||
S.Response.fail_raise ~code:400
|
S.Response.fail_raise ~code:400
|
||||||
"inflate: error during decompression:\n%s %s" e1 e2
|
"inflate: error during decompression:\n%s %s" e1 e2);
|
||||||
end;
|
S._debug (fun k ->
|
||||||
S._debug (fun k->k "inflate: refill %d bytes into internal buf" self.len);
|
k "inflate: refill %d bytes into internal buf" self.len)
|
||||||
);
|
))
|
||||||
)
|
|
||||||
()
|
()
|
||||||
|
|
||||||
let encode_deflate_stream_ ~buf_size (is:S.byte_stream) : S.byte_stream =
|
let encode_deflate_stream_ ~buf_size (is : S.byte_stream) : S.byte_stream =
|
||||||
S._debug (fun k->k "wrap stream with deflate.encode");
|
S._debug (fun k -> k "wrap stream with deflate.encode");
|
||||||
let refill = ref true in
|
let refill = ref true in
|
||||||
let zlib_str = Zlib.deflate_init 4 false in
|
let zlib_str = Zlib.deflate_init 4 false in
|
||||||
BS.make
|
BS.make ~bs:(Bytes.create buf_size)
|
||||||
~bs:(Bytes.create buf_size)
|
|
||||||
~close:(fun _self ->
|
~close:(fun _self ->
|
||||||
S._debug (fun k->k "deflate: close");
|
S._debug (fun k -> k "deflate: close");
|
||||||
Zlib.deflate_end zlib_str;
|
Zlib.deflate_end zlib_str;
|
||||||
BS.close is
|
BS.close is)
|
||||||
)
|
|
||||||
~consume:(fun self n ->
|
~consume:(fun self n ->
|
||||||
self.off <- self.off + n;
|
self.off <- self.off + n;
|
||||||
self.len <- self.len - n
|
self.len <- self.len - n)
|
||||||
)
|
|
||||||
~fill:(fun self ->
|
~fill:(fun self ->
|
||||||
let rec loop() =
|
let rec loop () =
|
||||||
S._debug (fun k->k "deflate.fill.iter out_off=%d out_len=%d"
|
S._debug (fun k ->
|
||||||
self.off self.len);
|
k "deflate.fill.iter out_off=%d out_len=%d" self.off self.len);
|
||||||
if self.len > 0 then (
|
if self.len > 0 then
|
||||||
() (* still the same slice, not consumed entirely by output *)
|
()
|
||||||
) else if not !refill then (
|
(* still the same slice, not consumed entirely by output *)
|
||||||
() (* empty slice, no refill *)
|
else if not !refill then
|
||||||
) else (
|
()
|
||||||
|
(* empty slice, no refill *)
|
||||||
|
else (
|
||||||
(* the output was entirely consumed, we need to do more work *)
|
(* the output was entirely consumed, we need to do more work *)
|
||||||
is.BS.fill_buf();
|
is.BS.fill_buf ();
|
||||||
if is.len > 0 then (
|
if is.len > 0 then (
|
||||||
(* try to decompress from input buffer *)
|
(* try to decompress from input buffer *)
|
||||||
let _finished, used_in, used_out =
|
let _finished, used_in, used_out =
|
||||||
Zlib.deflate zlib_str
|
Zlib.deflate zlib_str is.bs is.off is.len self.bs 0
|
||||||
is.bs is.off is.len
|
(Bytes.length self.bs) Zlib.Z_NO_FLUSH
|
||||||
self.bs 0 (Bytes.length self.bs)
|
|
||||||
Zlib.Z_NO_FLUSH
|
|
||||||
in
|
in
|
||||||
self.off <- 0;
|
self.off <- 0;
|
||||||
self.len <- used_out;
|
self.len <- used_out;
|
||||||
is.consume used_in;
|
is.consume used_in;
|
||||||
S._debug
|
S._debug (fun k ->
|
||||||
(fun k->k "encode %d bytes as %d bytes using deflate (finished: %b)"
|
k "encode %d bytes as %d bytes using deflate (finished: %b)"
|
||||||
used_in used_out _finished);
|
used_in used_out _finished);
|
||||||
if _finished then (
|
if _finished then (
|
||||||
S._debug (fun k->k "deflate: finished");
|
S._debug (fun k -> k "deflate: finished");
|
||||||
refill := false;
|
refill := false
|
||||||
);
|
);
|
||||||
loop()
|
loop ()
|
||||||
) else (
|
) else (
|
||||||
(* [is] is done, finish sending the data in current buffer *)
|
(* [is] is done, finish sending the data in current buffer *)
|
||||||
let _finished, used_in, used_out =
|
let _finished, used_in, used_out =
|
||||||
Zlib.deflate zlib_str
|
Zlib.deflate zlib_str is.bs is.off is.len self.bs 0
|
||||||
is.bs is.off is.len
|
(Bytes.length self.bs) Zlib.Z_FULL_FLUSH
|
||||||
self.bs 0 (Bytes.length self.bs)
|
|
||||||
Zlib.Z_FULL_FLUSH
|
|
||||||
in
|
in
|
||||||
assert (used_in = 0);
|
assert (used_in = 0);
|
||||||
self.off <- 0;
|
self.off <- 0;
|
||||||
self.len <- used_out;
|
self.len <- used_out;
|
||||||
if used_out = 0 then (
|
if used_out = 0 then refill := false;
|
||||||
refill := false;
|
loop ()
|
||||||
);
|
|
||||||
loop()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
in
|
in
|
||||||
try loop()
|
try loop ()
|
||||||
with Zlib.Error (e1,e2) ->
|
with Zlib.Error (e1, e2) ->
|
||||||
S.Response.fail_raise ~code:400
|
S.Response.fail_raise ~code:400
|
||||||
"deflate: error during compression:\n%s %s" e1 e2
|
"deflate: error during compression:\n%s %s" e1 e2)
|
||||||
)
|
|
||||||
()
|
()
|
||||||
|
|
||||||
let split_on_char ?(f=fun x->x) c s : string list =
|
let split_on_char ?(f = fun x -> x) c s : string list =
|
||||||
let rec loop acc i =
|
let rec loop acc i =
|
||||||
match String.index_from s i c with
|
match String.index_from s i c with
|
||||||
| exception Not_found ->
|
| exception Not_found ->
|
||||||
let acc =
|
let acc =
|
||||||
if i=String.length s then acc
|
if i = String.length s then
|
||||||
else f (String.sub s i (String.length s-i)) :: acc
|
acc
|
||||||
in List.rev acc
|
else
|
||||||
|
f (String.sub s i (String.length s - i)) :: acc
|
||||||
|
in
|
||||||
|
List.rev acc
|
||||||
| j ->
|
| j ->
|
||||||
let acc = f (String.sub s i (j-i)) :: acc in
|
let acc = f (String.sub s i (j - i)) :: acc in
|
||||||
loop acc (j+1)
|
loop acc (j + 1)
|
||||||
in
|
in
|
||||||
loop [] 0
|
loop [] 0
|
||||||
|
|
||||||
let accept_deflate (req:_ S.Request.t) =
|
let accept_deflate (req : _ S.Request.t) =
|
||||||
match
|
match S.Request.get_header req "Accept-Encoding" with
|
||||||
S.Request.get_header req "Accept-Encoding"
|
|
||||||
with
|
|
||||||
| Some s -> List.mem "deflate" @@ split_on_char ~f:String.trim ',' s
|
| Some s -> List.mem "deflate" @@ split_on_char ~f:String.trim ',' s
|
||||||
| None -> false
|
| None -> false
|
||||||
|
|
||||||
let has_deflate s =
|
let has_deflate s =
|
||||||
try Scanf.sscanf s "deflate, %s" (fun _ -> true)
|
try Scanf.sscanf s "deflate, %s" (fun _ -> true) with _ -> false
|
||||||
with _ -> false
|
|
||||||
|
|
||||||
(* decompress [req]'s body if needed *)
|
(* decompress [req]'s body if needed *)
|
||||||
let decompress_req_stream_ ~buf_size (req:BS.t S.Request.t) : _ S.Request.t =
|
let decompress_req_stream_ ~buf_size (req : BS.t S.Request.t) : _ S.Request.t =
|
||||||
match S.Request.get_header ~f:String.trim req "Transfer-Encoding" with
|
match S.Request.get_header ~f:String.trim req "Transfer-Encoding" with
|
||||||
(* TODO
|
(* TODO
|
||||||
| Some "gzip" ->
|
| Some "gzip" ->
|
||||||
|
|
@ -151,21 +136,17 @@ let decompress_req_stream_ ~buf_size (req:BS.t S.Request.t) : _ S.Request.t =
|
||||||
Some (req', decode_gzip_stream_)
|
Some (req', decode_gzip_stream_)
|
||||||
*)
|
*)
|
||||||
| Some s when has_deflate s ->
|
| Some s when has_deflate s ->
|
||||||
begin match Scanf.sscanf s "deflate, %s" (fun s -> s) with
|
(match Scanf.sscanf s "deflate, %s" (fun s -> s) with
|
||||||
| tr' ->
|
| tr' ->
|
||||||
let body' = S.Request.body req |> decode_deflate_stream_ ~buf_size in
|
let body' = S.Request.body req |> decode_deflate_stream_ ~buf_size in
|
||||||
req
|
req
|
||||||
|> S.Request.set_header "Transfer-Encoding" tr'
|
|> S.Request.set_header "Transfer-Encoding" tr'
|
||||||
|> S.Request.set_body body'
|
|> S.Request.set_body body'
|
||||||
| exception _ -> req
|
| exception _ -> req)
|
||||||
end
|
|
||||||
| _ -> req
|
| _ -> req
|
||||||
|
|
||||||
let compress_resp_stream_
|
let compress_resp_stream_ ~compress_above ~buf_size (req : _ S.Request.t)
|
||||||
~compress_above
|
(resp : S.Response.t) : S.Response.t =
|
||||||
~buf_size
|
|
||||||
(req:_ S.Request.t) (resp:S.Response.t) : S.Response.t =
|
|
||||||
|
|
||||||
(* headers for compressed stream *)
|
(* headers for compressed stream *)
|
||||||
let update_headers h =
|
let update_headers h =
|
||||||
h
|
h
|
||||||
|
|
@ -177,39 +158,31 @@ let compress_resp_stream_
|
||||||
match resp.body with
|
match resp.body with
|
||||||
| `String s when String.length s > compress_above ->
|
| `String s when String.length s > compress_above ->
|
||||||
(* big string, we compress *)
|
(* big string, we compress *)
|
||||||
S._debug
|
S._debug (fun k ->
|
||||||
(fun k->k "encode str response with deflate (size %d, threshold %d)"
|
k "encode str response with deflate (size %d, threshold %d)"
|
||||||
(String.length s) compress_above);
|
(String.length s) compress_above);
|
||||||
let body =
|
let body = encode_deflate_stream_ ~buf_size @@ BS.of_string s in
|
||||||
encode_deflate_stream_ ~buf_size @@ BS.of_string s
|
|
||||||
in
|
|
||||||
resp
|
resp
|
||||||
|> S.Response.update_headers update_headers
|
|> S.Response.update_headers update_headers
|
||||||
|> S.Response.set_body (`Stream body)
|
|> S.Response.set_body (`Stream body)
|
||||||
|
|
||||||
| `Stream str ->
|
| `Stream str ->
|
||||||
S._debug (fun k->k "encode stream response with deflate");
|
S._debug (fun k -> k "encode stream response with deflate");
|
||||||
resp
|
resp
|
||||||
|> S.Response.update_headers update_headers
|
|> S.Response.update_headers update_headers
|
||||||
|> S.Response.set_body (`Stream (encode_deflate_stream_ ~buf_size str))
|
|> S.Response.set_body (`Stream (encode_deflate_stream_ ~buf_size str))
|
||||||
|
|
||||||
| `String _ | `Void -> resp
|
| `String _ | `Void -> resp
|
||||||
) else resp
|
) else
|
||||||
|
resp
|
||||||
|
|
||||||
let middleware
|
let middleware ?(compress_above = 16 * 1024) ?(buf_size = 16 * 1_024) () :
|
||||||
?(compress_above=16 * 1024)
|
S.Middleware.t =
|
||||||
?(buf_size=16 * 1_024)
|
|
||||||
() : S.Middleware.t =
|
|
||||||
let buf_size = max buf_size 1_024 in
|
let buf_size = max buf_size 1_024 in
|
||||||
fun h req ~resp ->
|
fun h req ~resp ->
|
||||||
let req = decompress_req_stream_ ~buf_size req in
|
let req = decompress_req_stream_ ~buf_size req in
|
||||||
h req
|
h req ~resp:(fun response ->
|
||||||
~resp:(fun response ->
|
|
||||||
resp @@ compress_resp_stream_ ~buf_size ~compress_above req response)
|
resp @@ compress_resp_stream_ ~buf_size ~compress_above req response)
|
||||||
|
|
||||||
let setup
|
let setup ?compress_above ?buf_size server =
|
||||||
?compress_above ?buf_size server =
|
|
||||||
let m = middleware ?compress_above ?buf_size () in
|
let m = middleware ?compress_above ?buf_size () in
|
||||||
S._debug (fun k->k "setup gzip support");
|
S._debug (fun k -> k "setup gzip support");
|
||||||
S.add_middleware ~stage:`Encoding server m
|
S.add_middleware ~stage:`Encoding server m
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
|
|
||||||
val middleware :
|
val middleware :
|
||||||
?compress_above:int ->
|
?compress_above:int -> ?buf_size:int -> unit -> Tiny_httpd_server.Middleware.t
|
||||||
?buf_size:int -> unit ->
|
|
||||||
Tiny_httpd_server.Middleware.t
|
|
||||||
(** Middleware responsible for deflate compression/decompression.
|
(** Middleware responsible for deflate compression/decompression.
|
||||||
@since 0.11 *)
|
@since 0.11 *)
|
||||||
|
|
||||||
val setup :
|
val setup : ?compress_above:int -> ?buf_size:int -> Tiny_httpd_server.t -> unit
|
||||||
?compress_above:int ->
|
|
||||||
?buf_size:int -> Tiny_httpd_server.t -> unit
|
|
||||||
(** Install middleware for tiny_httpd to be able to encode/decode
|
(** Install middleware for tiny_httpd to be able to encode/decode
|
||||||
compressed streams
|
compressed streams
|
||||||
@param compress_above threshold above with string responses are compressed
|
@param compress_above threshold above with string responses are compressed
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(library
|
(library
|
||||||
(name tiny_httpd_camlzip)
|
(name tiny_httpd_camlzip)
|
||||||
(public_name tiny_httpd_camlzip)
|
(public_name tiny_httpd_camlzip)
|
||||||
|
|
|
||||||
9
src/dune
9
src/dune
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
(library
|
(library
|
||||||
(name tiny_httpd)
|
(name tiny_httpd)
|
||||||
(public_name tiny_httpd)
|
(public_name tiny_httpd)
|
||||||
|
|
@ -8,5 +7,9 @@
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets Tiny_httpd_html_.ml)
|
(targets Tiny_httpd_html_.ml)
|
||||||
(deps (:bin ./gen/gentags.exe))
|
(deps
|
||||||
(action (with-stdout-to %{targets} (run %{bin}))))
|
(:bin ./gen/gentags.exe))
|
||||||
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
|
(run %{bin}))))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,2 @@
|
||||||
(executable
|
(executable
|
||||||
(name gentags))
|
(name gentags))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
|
||||||
(* adapted from https://github.com/sindresorhus/html-tags (MIT licensed) *)
|
(* adapted from https://github.com/sindresorhus/html-tags (MIT licensed) *)
|
||||||
|
|
||||||
let pf = Printf.printf
|
let pf = Printf.printf
|
||||||
let spf = Printf.sprintf
|
let spf = Printf.sprintf
|
||||||
|
|
||||||
let void = [
|
let void =
|
||||||
|
[
|
||||||
"area";
|
"area";
|
||||||
"base";
|
"base";
|
||||||
"br";
|
"br";
|
||||||
|
|
@ -20,9 +20,10 @@ let void = [
|
||||||
"source";
|
"source";
|
||||||
"track";
|
"track";
|
||||||
"wbr";
|
"wbr";
|
||||||
]
|
]
|
||||||
|
|
||||||
let normal = [
|
let normal =
|
||||||
|
[
|
||||||
"a";
|
"a";
|
||||||
"abbr";
|
"abbr";
|
||||||
"address";
|
"address";
|
||||||
|
|
@ -140,7 +141,8 @@ let normal = [
|
||||||
"var";
|
"var";
|
||||||
"video";
|
"video";
|
||||||
"wbr";
|
"wbr";
|
||||||
] |> List.filter (fun s -> not (List.mem s void))
|
]
|
||||||
|
|> List.filter (fun s -> not (List.mem s void))
|
||||||
|
|
||||||
(* obtained via:
|
(* obtained via:
|
||||||
{[
|
{[
|
||||||
|
|
@ -150,7 +152,8 @@ let normal = [
|
||||||
]}
|
]}
|
||||||
on https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
on https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
||||||
*)
|
*)
|
||||||
let attrs = [
|
let attrs =
|
||||||
|
[
|
||||||
"accept";
|
"accept";
|
||||||
"accept-charset";
|
"accept-charset";
|
||||||
"accesskey";
|
"accesskey";
|
||||||
|
|
@ -275,9 +278,10 @@ let attrs = [
|
||||||
"value";
|
"value";
|
||||||
"width";
|
"width";
|
||||||
"wrap";
|
"wrap";
|
||||||
]
|
]
|
||||||
|
|
||||||
let prelude = {|
|
let prelude =
|
||||||
|
{|
|
||||||
(** Output for HTML combinators.
|
(** Output for HTML combinators.
|
||||||
|
|
||||||
This output type is used to produce a string reasonably efficiently from
|
This output type is used to produce a string reasonably efficiently from
|
||||||
|
|
@ -431,11 +435,17 @@ let oname = function
|
||||||
| "Text" -> "text"
|
| "Text" -> "text"
|
||||||
| "type" -> "type_"
|
| "type" -> "type_"
|
||||||
| name ->
|
| name ->
|
||||||
String.map (function '-' -> '_' | c -> c) name
|
String.map
|
||||||
|
(function
|
||||||
|
| '-' -> '_'
|
||||||
|
| c -> c)
|
||||||
|
name
|
||||||
|
|
||||||
let emit_void name =
|
let emit_void name =
|
||||||
let oname = oname name in
|
let oname = oname name in
|
||||||
pf "(** tag %S, see {{:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/%s} mdn} *)\n"
|
pf
|
||||||
|
"(** tag %S, see \
|
||||||
|
{{:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/%s} mdn} *)\n"
|
||||||
name name;
|
name name;
|
||||||
pf "let %s : void = fun ?(if_=true) attrs out ->\n" oname;
|
pf "let %s : void = fun ?(if_=true) attrs out ->\n" oname;
|
||||||
pf " if if_ then (\n";
|
pf " if if_ then (\n";
|
||||||
|
|
@ -447,12 +457,14 @@ let emit_void name =
|
||||||
let emit_normal name =
|
let emit_normal name =
|
||||||
let oname = oname name in
|
let oname = oname name in
|
||||||
|
|
||||||
pf "(** tag %S, see {{:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/%s} mdn} *)\n"
|
pf
|
||||||
|
"(** tag %S, see \
|
||||||
|
{{:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/%s} mdn} *)\n"
|
||||||
name name;
|
name name;
|
||||||
pf "let %s : nary = fun ?(if_=true) attrs sub out ->\n" oname;
|
pf "let %s : nary = fun ?(if_=true) attrs sub out ->\n" oname;
|
||||||
pf " if if_ then (\n";
|
pf " if if_ then (\n";
|
||||||
(* for <pre>, newlines actually matter *)
|
(* for <pre>, newlines actually matter *)
|
||||||
if name="pre" then pf " Out.with_no_format_nl out @@ fun () ->\n";
|
if name = "pre" then pf " Out.with_no_format_nl out @@ fun () ->\n";
|
||||||
pf " _write_tag_attrs ~void:false out %S attrs;\n" name;
|
pf " _write_tag_attrs ~void:false out %S attrs;\n" name;
|
||||||
pf " List.iter (fun sub -> Out.add_format_nl out; sub out) sub;\n";
|
pf " List.iter (fun sub -> Out.add_format_nl out; sub out) sub;\n";
|
||||||
pf " if sub <> [] then Out.add_format_nl out;\n";
|
pf " if sub <> [] then Out.add_format_nl out;\n";
|
||||||
|
|
@ -461,21 +473,23 @@ let emit_normal name =
|
||||||
|
|
||||||
(* block version *)
|
(* block version *)
|
||||||
let oname = oname ^ "'" in
|
let oname = oname ^ "'" in
|
||||||
pf "(** tag %S, see {{:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/%s} mdn} *)\n"
|
pf
|
||||||
|
"(** tag %S, see \
|
||||||
|
{{:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/%s} mdn} *)\n"
|
||||||
name name;
|
name name;
|
||||||
pf "let %s : nary' = fun ?(if_=true) attrs l out ->\n" oname;
|
pf "let %s : nary' = fun ?(if_=true) attrs l out ->\n" oname;
|
||||||
pf " if if_ then (\n";
|
pf " if if_ then (\n";
|
||||||
if name="pre" then pf " Out.with_no_format_nl out @@ fun () ->\n";
|
if name = "pre" then pf " Out.with_no_format_nl out @@ fun () ->\n";
|
||||||
pf " _write_tag_attrs ~void:false out %S attrs;\n" name;
|
pf " _write_tag_attrs ~void:false out %S attrs;\n" name;
|
||||||
pf " let has_sub = _write_subs out l in\n";
|
pf " let has_sub = _write_subs out l in\n";
|
||||||
pf " if has_sub then Out.add_format_nl out;\n";
|
pf " if has_sub then Out.add_format_nl out;\n";
|
||||||
pf " Out.add_string out \"</%s>\")" name;
|
pf " Out.add_string out \"</%s>\")" name;
|
||||||
pf "\n\n";
|
pf "\n\n";
|
||||||
|
|
||||||
|
|
||||||
()
|
()
|
||||||
|
|
||||||
let doc_attrs = {|Attributes.
|
let doc_attrs =
|
||||||
|
{|Attributes.
|
||||||
|
|
||||||
This module contains combinator for the standard attributes.
|
This module contains combinator for the standard attributes.
|
||||||
One can also just use a pair of strings. |}
|
One can also just use a pair of strings. |}
|
||||||
|
|
@ -498,4 +512,3 @@ let () =
|
||||||
List.iter emit_attr attrs;
|
List.iter emit_attr attrs;
|
||||||
pf "end\n";
|
pf "end\n";
|
||||||
()
|
()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
|
|
||||||
(executable
|
(executable
|
||||||
(name qtest)
|
(name qtest)
|
||||||
(modes native)
|
(modes native)
|
||||||
(flags :standard -warn-error -a+8 -w -33)
|
(flags :standard -warn-error -a+8 -w -33)
|
||||||
(libraries qcheck-core qcheck ounit2
|
(libraries qcheck-core qcheck ounit2 threads threads.posix tiny_httpd))
|
||||||
threads threads.posix tiny_httpd))
|
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(deps (glob_files ../*.ml{,i}))
|
(deps
|
||||||
|
(glob_files ../*.ml{,i}))
|
||||||
(targets qtest.ml)
|
(targets qtest.ml)
|
||||||
(action (run qtest extract --quiet %{deps} -o %{targets})))
|
(action
|
||||||
|
(run qtest extract --quiet %{deps} -o %{targets})))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(action (run ./qtest.exe)))
|
(action
|
||||||
|
(run ./qtest.exe)))
|
||||||
|
|
|
||||||
72
tests/dune
72
tests/dune
|
|
@ -1,64 +1,92 @@
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets echo1.out)
|
(targets echo1.out)
|
||||||
(deps (:bin ../examples/echo.exe))
|
(deps
|
||||||
|
(:bin ../examples/echo.exe))
|
||||||
(locks /port)
|
(locks /port)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
|
(= %{system} "linux"))
|
||||||
(package tiny_httpd_camlzip)
|
(package tiny_httpd_camlzip)
|
||||||
(action (with-stdout-to %{targets} (run ./echo1.sh %{bin}))))
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
|
(run ./echo1.sh %{bin}))))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(package tiny_httpd_camlzip)
|
(package tiny_httpd_camlzip)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
(action (diff echo1.expect echo1.out)))
|
(= %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(diff echo1.expect echo1.out)))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets sse_count.out)
|
(targets sse_count.out)
|
||||||
(deps (:bin ../examples/sse_server.exe))
|
(deps
|
||||||
|
(:bin ../examples/sse_server.exe))
|
||||||
(locks /port)
|
(locks /port)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
|
(= %{system} "linux"))
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(action (with-stdout-to %{targets} (run ./sse_count.sh %{bin}))))
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
|
(run ./sse_count.sh %{bin}))))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
(action (diff sse_count.expect sse_count.out)))
|
(= %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(diff sse_count.expect sse_count.out)))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets upload-out)
|
(targets upload-out)
|
||||||
(deps (:bin ../src/bin/http_of_dir.exe) foo_50)
|
(deps
|
||||||
|
(:bin ../src/bin/http_of_dir.exe)
|
||||||
|
foo_50)
|
||||||
(locks /port)
|
(locks /port)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
(action (with-stdout-to %{targets}
|
(= %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
(run ./upload_chunked.sh %{bin}))))
|
(run ./upload_chunked.sh %{bin}))))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
(action (diff upload-out.expect upload-out)))
|
(= %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(diff upload-out.expect upload-out)))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets dl-out)
|
(targets dl-out)
|
||||||
(deps (:bin ../src/bin/http_of_dir.exe) foo_50)
|
(deps
|
||||||
|
(:bin ../src/bin/http_of_dir.exe)
|
||||||
|
foo_50)
|
||||||
(locks /port)
|
(locks /port)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
(action (with-stdout-to %{targets}
|
(= %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
(run ./download_chunked.sh %{bin}))))
|
(run ./download_chunked.sh %{bin}))))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(package tiny_httpd)
|
(package tiny_httpd)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
(action (diff dl-out.expect dl-out)))
|
(= %{system} "linux"))
|
||||||
|
(action
|
||||||
|
(diff dl-out.expect dl-out)))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets foo_50)
|
(targets foo_50)
|
||||||
(enabled_if (= %{system} "linux"))
|
(enabled_if
|
||||||
|
(= %{system} "linux"))
|
||||||
(action
|
(action
|
||||||
(bash "dd if=/dev/zero of=%{targets} bs=1M count=50")))
|
(bash "dd if=/dev/zero of=%{targets} bs=1M count=50")))
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,31 @@
|
||||||
|
|
||||||
(executable
|
(executable
|
||||||
(libraries tiny_httpd)
|
(libraries tiny_httpd)
|
||||||
(name makehtml))
|
(name makehtml))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets t1.out.html)
|
(targets t1.out.html)
|
||||||
(deps (:bin ./makehtml.exe))
|
(deps
|
||||||
(action (with-stdout-to %{targets} (run %{bin} 1))))
|
(:bin ./makehtml.exe))
|
||||||
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
|
(run %{bin} 1))))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(action (diff t1.expected.html t1.out.html)))
|
(action
|
||||||
|
(diff t1.expected.html t1.out.html)))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(targets t2.out.html)
|
(targets t2.out.html)
|
||||||
(deps (:bin ./makehtml.exe))
|
(deps
|
||||||
(action (with-stdout-to %{targets} (run %{bin} 2))))
|
(:bin ./makehtml.exe))
|
||||||
|
(action
|
||||||
|
(with-stdout-to
|
||||||
|
%{targets}
|
||||||
|
(run %{bin} 2))))
|
||||||
|
|
||||||
(rule
|
(rule
|
||||||
(alias runtest)
|
(alias runtest)
|
||||||
(action (diff t2.expected.html t2.out.html)))
|
(action
|
||||||
|
(diff t2.expected.html t2.out.html)))
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,52 @@
|
||||||
open Tiny_httpd_html
|
open Tiny_httpd_html
|
||||||
|
|
||||||
let spf = Printf.sprintf
|
let spf = Printf.sprintf
|
||||||
|
|
||||||
let list_init n f =
|
let list_init n f =
|
||||||
let rec loop i =
|
let rec loop i =
|
||||||
if i=n then []
|
if i = n then
|
||||||
else f i :: loop (i+1)
|
[]
|
||||||
in loop 0
|
else
|
||||||
|
f i :: loop (i + 1)
|
||||||
|
in
|
||||||
|
loop 0
|
||||||
|
|
||||||
let t1() =
|
let t1 () =
|
||||||
html [] [
|
html []
|
||||||
|
[
|
||||||
head [] [];
|
head [] [];
|
||||||
body [] [
|
body []
|
||||||
ul [A.style "list-style: circle"] (
|
[
|
||||||
li[][pre [] [txt "a"; pre[][txt "c"; txt"d"]; txt "b"]] ::
|
ul
|
||||||
list_init 100 (fun i -> li [A.id (spf "l%d" i)] [txt (spf "item %d" i)])
|
[ A.style "list-style: circle" ]
|
||||||
)
|
(li [] [ pre [] [ txt "a"; pre [] [ txt "c"; txt "d" ]; txt "b" ] ]
|
||||||
]
|
:: list_init 100 (fun i ->
|
||||||
|
li [ A.id (spf "l%d" i) ] [ txt (spf "item %d" i) ]));
|
||||||
|
];
|
||||||
]
|
]
|
||||||
|
|
||||||
|
let t2 () =
|
||||||
let t2() =
|
html []
|
||||||
html [] [
|
[
|
||||||
head [] [];
|
head [] [];
|
||||||
pre [] [txt "a"; txt "b"];
|
pre [] [ txt "a"; txt "b" ];
|
||||||
body [] [
|
body []
|
||||||
ul' [A.style "list-style: circle"] [
|
[
|
||||||
sub_l @@ list_init 100 @@ fun i ->
|
ul'
|
||||||
li ~if_:(i<> 42) [A.id (spf "l%d" i)] [txt (spf "item %d" i)]
|
[ A.style "list-style: circle" ]
|
||||||
]
|
[
|
||||||
]
|
(sub_l @@ list_init 100
|
||||||
|
@@ fun i ->
|
||||||
|
li ~if_:(i <> 42) [ A.id (spf "l%d" i) ] [ txt (spf "item %d" i) ]
|
||||||
|
);
|
||||||
|
];
|
||||||
|
];
|
||||||
]
|
]
|
||||||
|
|
||||||
let render t =
|
let render t = print_endline @@ to_string_top @@ t
|
||||||
print_endline @@ to_string_top @@ t
|
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
match Sys.argv.(1) with
|
match Sys.argv.(1) with
|
||||||
| "1" -> render @@ t1()
|
| "1" -> render @@ t1 ()
|
||||||
| "2" -> render @@ t2()
|
| "2" -> render @@ t2 ()
|
||||||
| _ -> failwith "unknown cmd"
|
| _ -> failwith "unknown cmd"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue