diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 00000000..67cabbe6
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "vendor/iostream"]
+ path = vendor/iostream
+ url = https://github.com/c-cube/ocaml-iostream
diff --git a/dune-project b/dune-project
index 301047b7..370ef50a 100644
--- a/dune-project
+++ b/dune-project
@@ -21,6 +21,8 @@
seq
base-threads
result
+ hmap
+ (iostream (>= 0.2))
(ocaml (>= 4.08))
(odoc :with-doc)
(logs :with-test)
@@ -34,5 +36,6 @@
(depends
(tiny_httpd (= :version))
(camlzip (>= 1.06))
+ iostream-camlzip
(logs :with-test)
(odoc :with-doc)))
diff --git a/examples/echo.ml b/examples/echo.ml
index a8b1b232..f3d0f2af 100644
--- a/examples/echo.ml
+++ b/examples/echo.ml
@@ -1,4 +1,4 @@
-module S = Tiny_httpd
+open Tiny_httpd_core
module Log = Tiny_httpd.Log
let now_ = Unix.gettimeofday
@@ -34,7 +34,7 @@ let alice_text =
sides of the well, and noticed that they were filled with cupboards......"
(* util: a little middleware collecting statistics *)
-let middleware_stat () : S.Middleware.t * (unit -> string) =
+let middleware_stat () : Server.Middleware.t * (unit -> string) =
let n_req = ref 0 in
let total_time_ = ref 0. in
let parse_time_ = ref 0. in
@@ -43,7 +43,7 @@ let middleware_stat () : S.Middleware.t * (unit -> string) =
let m h req ~resp =
incr n_req;
- let t1 = S.Request.start_time req in
+ let t1 = Request.start_time req in
let t2 = now_ () in
h req ~resp:(fun response ->
let t3 = now_ () in
@@ -92,23 +92,23 @@ let () =
(fun _ -> raise (Arg.Bad ""))
"echo [option]*";
- let server = S.create ~port:!port_ ~max_connections:!j () in
+ let server = Tiny_httpd.create ~port:!port_ ~max_connections:!j () in
Tiny_httpd_camlzip.setup ~compress_above:1024 ~buf_size:(16 * 1024) server;
let m_stats, get_stats = middleware_stat () in
- S.add_middleware server ~stage:(`Stage 1) m_stats;
+ Server.add_middleware server ~stage:(`Stage 1) m_stats;
(* say hello *)
- S.add_route_handler ~meth:`GET server
- S.Route.(exact "hello" @/ string @/ return)
- (fun name _req -> S.Response.make_string (Ok ("hello " ^ name ^ "!\n")));
+ Server.add_route_handler ~meth:`GET server
+ Route.(exact "hello" @/ string @/ return)
+ (fun name _req -> Response.make_string (Ok ("hello " ^ name ^ "!\n")));
(* compressed file access *)
- S.add_route_handler ~meth:`GET server
- S.Route.(exact "zcat" @/ string_urlencoded @/ return)
+ Server.add_route_handler ~meth:`GET server
+ Route.(exact "zcat" @/ string_urlencoded @/ return)
(fun path _req ->
let ic = open_in path in
- let str = S.Byte_stream.of_chan ic in
+ let str = IO.Input.of_in_channel ic in
let mime_type =
try
let p = Unix.open_process_in (Printf.sprintf "file -i -b %S" path) in
@@ -121,42 +121,42 @@ let () =
[]
with _ -> []
in
- S.Response.make_stream ~headers:mime_type (Ok str));
+ Response.make_stream ~headers:mime_type (Ok str));
(* echo request *)
- S.add_route_handler server
- S.Route.(exact "echo" @/ return)
+ Server.add_route_handler server
+ Route.(exact "echo" @/ return)
(fun req ->
let q =
- S.Request.query req
+ Request.query req
|> List.map (fun (k, v) -> Printf.sprintf "%S = %S" k v)
|> String.concat ";"
in
- S.Response.make_string
- (Ok (Format.asprintf "echo:@ %a@ (query: %s)@." S.Request.pp req q)));
+ Response.make_string
+ (Ok (Format.asprintf "echo:@ %a@ (query: %s)@." Request.pp req q)));
(* file upload *)
- S.add_route_handler_stream ~meth:`PUT server
- S.Route.(exact "upload" @/ string @/ return)
+ Server.add_route_handler_stream ~meth:`PUT server
+ Route.(exact "upload" @/ string @/ return)
(fun path req ->
Log.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" Headers.pp (Request.headers req)));
try
let oc = open_out @@ "/tmp/" ^ path in
- S.Byte_stream.to_chan oc req.S.Request.body;
+ IO.Input.to_chan oc req.Request.body;
flush oc;
- S.Response.make_string (Ok "uploaded file")
+ Response.make_string (Ok "uploaded file")
with e ->
- S.Response.fail ~code:500 "couldn't upload file: %s"
+ Response.fail ~code:500 "couldn't upload file: %s"
(Printexc.to_string e));
(* protected by login *)
- S.add_route_handler server
- S.Route.(exact "protected" @/ return)
+ Server.add_route_handler server
+ Route.(exact "protected" @/ return)
(fun req ->
let ok =
- match S.Request.get_header req "authorization" with
+ match Request.get_header req "authorization" with
| Some v ->
Log.debug (fun k -> k "authenticate with %S" v);
v = "Basic " ^ base64 "user:foobar"
@@ -167,40 +167,40 @@ let () =
let s =
"
hello, this is super secret!
log out"
in
- S.Response.make_string (Ok s)
+ Response.make_string (Ok s)
) else (
let headers =
- S.Headers.(empty |> set "www-authenticate" "basic realm=\"echo\"")
+ Headers.(empty |> set "www-authenticate" "basic realm=\"echo\"")
in
- S.Response.fail ~code:401 ~headers "invalid"
+ Response.fail ~code:401 ~headers "invalid"
));
(* logout *)
- S.add_route_handler server
- S.Route.(exact "logout" @/ return)
- (fun _req -> S.Response.fail ~code:401 "logged out");
+ Server.add_route_handler server
+ Route.(exact "logout" @/ return)
+ (fun _req -> Response.fail ~code:401 "logged out");
(* stats *)
- S.add_route_handler server
- S.Route.(exact "stats" @/ return)
+ Server.add_route_handler server
+ Route.(exact "stats" @/ return)
(fun _req ->
let stats = get_stats () in
- S.Response.make_string @@ Ok stats);
+ Response.make_string @@ Ok stats);
- S.add_route_handler server
- S.Route.(exact "alice" @/ return)
- (fun _req -> S.Response.make_string (Ok alice_text));
+ Server.add_route_handler server
+ Route.(exact "alice" @/ return)
+ (fun _req -> Response.make_string (Ok alice_text));
(* VFS *)
- Tiny_httpd_dir.add_vfs server
+ Tiny_httpd.Dir.add_vfs server
~config:
- (Tiny_httpd_dir.config ~download:true
- ~dir_behavior:Tiny_httpd_dir.Index_or_lists ())
+ (Tiny_httpd.Dir.config ~download:true
+ ~dir_behavior:Tiny_httpd.Dir.Index_or_lists ())
~vfs:Vfs.vfs ~prefix:"vfs";
(* main page *)
- S.add_route_handler server
- S.Route.(return)
+ Server.add_route_handler server
+ Route.(return)
(fun _req ->
let open Tiny_httpd_html in
let h =
@@ -272,9 +272,10 @@ let () =
]
in
let s = to_string_top h in
- S.Response.make_string ~headers:[ "content-type", "text/html" ] @@ Ok 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);
- match S.run server with
+ Printf.printf "listening on http://%s:%d\n%!" (Server.addr server)
+ (Server.port server);
+ match Server.run server with
| Ok () -> ()
| Error e -> raise e
diff --git a/examples/echo_ws.ml b/examples/echo_ws.ml
index 5a616d3f..f24cf283 100644
--- a/examples/echo_ws.ml
+++ b/examples/echo_ws.ml
@@ -1,6 +1,5 @@
module S = Tiny_httpd
-module Log = Tiny_httpd.Log
-module IO = Tiny_httpd_io
+open Tiny_httpd_core
let setup_logging ~debug () =
Logs.set_reporter @@ Logs.format_reporter ();
@@ -13,8 +12,7 @@ let setup_logging ~debug () =
let handle_ws _client_addr ic oc =
Log.info (fun k ->
- k "new client connection from %s"
- (Tiny_httpd_util.show_sockaddr _client_addr));
+ k "new client connection from %s" (Util.show_sockaddr _client_addr));
let (_ : Thread.t) =
Thread.create
@@ -58,7 +56,7 @@ let () =
let server = S.create ~port:!port_ ~max_connections:!j () in
Tiny_httpd_ws.add_route_handler server
- S.Route.(exact "echo" @/ return)
+ Route.(exact "echo" @/ return)
handle_ws;
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
diff --git a/examples/sse_server.ml b/examples/sse_server.ml
index 650c05cd..c458026a 100644
--- a/examples/sse_server.ml
+++ b/examples/sse_server.ml
@@ -1,7 +1,6 @@
(* serves some streams of events *)
-module S = Tiny_httpd
-module Log = Tiny_httpd_log
+open Tiny_httpd_core
let port = ref 8080
@@ -14,7 +13,7 @@ let () =
])
(fun _ -> ())
"sse_clock [opt*]";
- let server = S.create ~port:!port () in
+ let server = Tiny_httpd.create ~port:!port () in
let extra_headers =
[
@@ -24,9 +23,9 @@ let () =
in
(* tick/tock goes the clock *)
- S.add_route_server_sent_handler server
- S.Route.(exact "clock" @/ return)
- (fun _req (module EV : S.SERVER_SENT_GENERATOR) ->
+ Server.add_route_server_sent_handler server
+ Route.(exact "clock" @/ return)
+ (fun _req (module EV : Server.SERVER_SENT_GENERATOR) ->
Log.debug (fun k -> k "new SSE connection");
EV.set_headers extra_headers;
let tick = ref true in
@@ -47,26 +46,26 @@ let () =
done);
(* just count *)
- S.add_route_server_sent_handler server
- S.Route.(exact "count" @/ return)
- (fun _req (module EV : S.SERVER_SENT_GENERATOR) ->
+ Server.add_route_server_sent_handler server
+ Route.(exact "count" @/ return)
+ (fun _req (module EV : Server.SERVER_SENT_GENERATOR) ->
let n = ref 0 in
while true do
EV.send_event ~data:(string_of_int !n) ();
incr n;
Unix.sleepf 0.1
done);
- S.add_route_server_sent_handler server
- S.Route.(exact "count" @/ int @/ return)
- (fun n _req (module EV : S.SERVER_SENT_GENERATOR) ->
+ Server.add_route_server_sent_handler server
+ Route.(exact "count" @/ int @/ return)
+ (fun n _req (module EV : Server.SERVER_SENT_GENERATOR) ->
for i = 0 to n do
EV.send_event ~data:(string_of_int i) ();
Unix.sleepf 0.1
done;
EV.close ());
- Printf.printf "listening on http://localhost:%d/\n%!" (S.port server);
- match S.run server with
+ Printf.printf "listening on http://localhost:%d/\n%!" (Server.port server);
+ match Server.run server with
| Ok () -> ()
| Error e ->
Printf.eprintf "error: %s\n%!" (Printexc.to_string e);
diff --git a/examples/writer.ml b/examples/writer.ml
index 9911ac3b..fed4eb60 100644
--- a/examples/writer.ml
+++ b/examples/writer.ml
@@ -1,7 +1,8 @@
module H = Tiny_httpd
+open Tiny_httpd_core
let serve_zeroes server : unit =
- H.add_route_handler server H.(Route.(exact "zeroes" @/ int @/ return))
+ Server.add_route_handler server Route.(exact "zeroes" @/ int @/ return)
@@ fun n _req ->
(* stream [n] zeroes *)
let write (oc : H.IO.Output.t) : unit =
@@ -11,7 +12,7 @@ let serve_zeroes server : unit =
done
in
let writer = H.IO.Writer.make ~write () in
- H.Response.make_writer @@ Ok writer
+ Response.make_writer @@ Ok writer
let serve_file server : unit =
H.add_route_handler server H.(Route.(exact "file" @/ string @/ return))
@@ -32,9 +33,9 @@ let serve_file server : unit =
in
let writer = H.IO.Writer.make ~write () in
- H.Response.make_writer @@ Ok writer
+ Response.make_writer @@ Ok writer
) else
- H.Response.fail ~code:404 "file not found"
+ Response.fail ~code:404 "file not found"
let () =
let port = ref 8085 in
@@ -43,7 +44,7 @@ let () =
Printf.printf "listen on http://localhost:%d/\n%!" !port;
serve_file server;
serve_zeroes server;
- H.add_route_handler server H.Route.return (fun _req ->
+ H.add_route_handler server Route.return (fun _req ->
let body =
H.Html.(
div []
@@ -58,5 +59,5 @@ let () =
])
|> H.Html.to_string_top
in
- H.Response.make_string @@ Ok body);
+ Response.make_string @@ Ok body);
H.run_exn server
diff --git a/src/Tiny_httpd.ml b/src/Tiny_httpd.ml
index dfdd3b46..cbffe69b 100644
--- a/src/Tiny_httpd.ml
+++ b/src/Tiny_httpd.ml
@@ -1,9 +1,68 @@
-module Buf = Tiny_httpd_buf
-module Byte_stream = Tiny_httpd_stream
-include Tiny_httpd_server
-module Util = Tiny_httpd_util
-module Dir = Tiny_httpd_dir
+module Buf = Buf
module Html = Tiny_httpd_html
-module IO = Tiny_httpd_io
-module Pool = Tiny_httpd_pool
-module Log = Tiny_httpd_log
+module IO = Tiny_httpd_core.IO
+module Request = Tiny_httpd_core.Request
+module Response = Tiny_httpd_core.Response
+module Response_code = Tiny_httpd_core.Response_code
+module Route = Tiny_httpd_core.Route
+module Headers = Tiny_httpd_core.Headers
+module Meth = Tiny_httpd_core.Meth
+module Pool = Tiny_httpd_core.Pool
+module Log = Tiny_httpd_core.Log
+module Server = Tiny_httpd_core.Server
+module Util = Tiny_httpd_core.Util
+include Server
+module Dir = Tiny_httpd_unix.Dir
+
+module type VFS = Tiny_httpd_unix.Dir.VFS
+
+open struct
+ let get_max_connection_ ?(max_connections = 64) () : int =
+ let max_connections = max 4 max_connections in
+ max_connections
+
+ let clear_slice (slice : IO.Slice.t) =
+ Bytes.fill slice.bytes 0 (Bytes.length slice.bytes) '\x00';
+ slice.off <- 0;
+ slice.len <- 0
+end
+
+let create ?(masksigpipe = true) ?max_connections ?(timeout = 0.0) ?buf_size
+ ?(get_time_s = Unix.gettimeofday)
+ ?(new_thread = fun f -> ignore (Thread.create f () : Thread.t))
+ ?(addr = "127.0.0.1") ?(port = 8080) ?sock ?middlewares () : t =
+ let max_connections = get_max_connection_ ?max_connections () in
+ let server =
+ {
+ Tiny_httpd_unix.Unix_tcp_server_.addr;
+ new_thread;
+ buf_pool =
+ Pool.create ~clear:Buf.clear_and_zero
+ ~mk_item:(fun () -> Buf.create ?size:buf_size ())
+ ();
+ slice_pool =
+ Pool.create ~clear:clear_slice
+ ~mk_item:
+ (let buf_size = Option.value buf_size ~default:4096 in
+ fun () -> IO.Slice.create buf_size)
+ ();
+ running = true;
+ port;
+ sock;
+ max_connections;
+ sem_max_connections = Tiny_httpd_unix.Sem.create max_connections;
+ masksigpipe;
+ timeout;
+ }
+ in
+ let tcp_server_builder =
+ Tiny_httpd_unix.Unix_tcp_server_.to_tcp_server server
+ in
+ let module B = struct
+ let init_addr () = addr
+ let init_port () = port
+ let get_time_s = get_time_s
+ let tcp_server () = tcp_server_builder
+ end in
+ let backend = (module B : IO_BACKEND) in
+ Server.create_from ?buf_size ?middlewares ~backend ()
diff --git a/src/Tiny_httpd.mli b/src/Tiny_httpd.mli
index b4cc6e89..2490646e 100644
--- a/src/Tiny_httpd.mli
+++ b/src/Tiny_httpd.mli
@@ -79,39 +79,94 @@ echo:
processing streams and parsing requests.
*)
-module Buf = Tiny_httpd_buf
-
-(** {2 Generic byte streams} *)
-
-module Byte_stream = Tiny_httpd_stream
+module Buf = Buf
(** {2 IO Abstraction} *)
-module IO = Tiny_httpd_io
+module IO = Tiny_httpd_core.IO
(** {2 Logging *)
-module Log = Tiny_httpd_log
-
-(** {2 Main Server Type} *)
-
-(** @inline *)
-include module type of struct
- include Tiny_httpd_server
-end
+module Log = Tiny_httpd_core.Log
(** {2 Utils} *)
-module Util = Tiny_httpd_util
+module Util = Tiny_httpd_core.Util
(** {2 Resource pool} *)
-module Pool = Tiny_httpd_pool
+module Pool = Tiny_httpd_core.Pool
(** {2 Static directory serving} *)
-module Dir = Tiny_httpd_dir
+module Dir = Tiny_httpd_unix.Dir
+
+module type VFS = Tiny_httpd_unix.Dir.VFS
+
+(** {2 HTML combinators} *)
module Html = Tiny_httpd_html
(** Alias to {!Tiny_httpd_html}
@since 0.12 *)
+
+(** {2 Main server types} *)
+
+module Request = Tiny_httpd_core.Request
+module Response = Tiny_httpd_core.Response
+module Response_code = Tiny_httpd_core.Response_code
+module Route = Tiny_httpd_core.Route
+module Headers = Tiny_httpd_core.Headers
+module Meth = Tiny_httpd_core.Meth
+module Server = Tiny_httpd_core.Server
+
+(** @inline *)
+include module type of struct
+ include Server
+end
+
+val create :
+ ?masksigpipe:bool ->
+ ?max_connections:int ->
+ ?timeout:float ->
+ ?buf_size:int ->
+ ?get_time_s:(unit -> float) ->
+ ?new_thread:((unit -> unit) -> unit) ->
+ ?addr:string ->
+ ?port:int ->
+ ?sock:Unix.file_descr ->
+ ?middlewares:([ `Encoding | `Stage of int ] * Middleware.t) list ->
+ unit ->
+ t
+(** Create a new webserver using UNIX abstractions.
+
+ The server will not do anything until {!run} is called on it.
+ Before starting the server, one can use {!add_path_handler} and
+ {!set_top_handler} to specify how to handle incoming requests.
+
+ @param masksigpipe if true, block the signal {!Sys.sigpipe} which otherwise
+ tends to kill client threads when they try to write on broken sockets. Default: [true].
+
+ @param buf_size size for buffers (since 0.11)
+
+ @param new_thread a function used to spawn a new thread to handle a
+ new client connection. By default it is {!Thread.create} but one
+ could use a thread pool instead.
+ See for example {{: https://github.com/c-cube/tiny-httpd-moonpool-bench/blob/0dcbbffb4fe34ea4ad79d46343ad0cebb69ca69f/examples/t1.ml#L31}
+ this use of moonpool}.
+
+ @param middlewares see {!add_middleware} for more details.
+
+ @param max_connections maximum number of simultaneous connections.
+ @param timeout connection is closed if the socket does not do read or
+ write for the amount of second. Default: 0.0 which means no timeout.
+ timeout is not recommended when using proxy.
+ @param addr address (IPv4 or IPv6) to listen on. Default ["127.0.0.1"].
+ @param port to listen on. Default [8080].
+ @param sock an existing socket given to the server to listen on, e.g. by
+ systemd on Linux (or launchd on macOS). If passed in, this socket will be
+ used instead of the [addr] and [port]. If not passed in, those will be
+ used. This parameter exists since 0.10.
+
+ @param get_time_s obtain the current timestamp in seconds.
+ This parameter exists since 0.11.
+*)
diff --git a/src/Tiny_httpd_io.ml b/src/Tiny_httpd_io.ml
deleted file mode 100644
index 407f5108..00000000
--- a/src/Tiny_httpd_io.ml
+++ /dev/null
@@ -1,370 +0,0 @@
-(** IO abstraction.
-
- We abstract IO so we can support classic unix blocking IOs
- with threads, and modern async IO with Eio.
-
- {b NOTE}: experimental.
-
- @since 0.14
-*)
-
-module Buf = Tiny_httpd_buf
-
-(** Input channel (byte source) *)
-module Input = struct
- type t = {
- input: bytes -> int -> int -> int;
- (** Read into the slice. Returns [0] only if the
- channel is closed. *)
- close: unit -> unit; (** Close the input. Must be idempotent. *)
- }
- (** An input channel, i.e an incoming stream of bytes.
-
- This can be a [string], an [int_channel], an [Unix.file_descr], a
- decompression wrapper around another input channel, etc. *)
-
- let of_in_channel ?(close_noerr = false) (ic : in_channel) : t =
- {
- input = (fun buf i len -> input ic buf i len);
- close =
- (fun () ->
- if close_noerr then
- close_in_noerr ic
- else
- close_in ic);
- }
-
- let of_unix_fd ?(close_noerr = false) ~closed (fd : Unix.file_descr) : t =
- let eof = ref false in
- {
- input =
- (fun buf i len ->
- let n = ref 0 in
- if (not !eof) && len > 0 then (
- let continue = ref true in
- while !continue do
- (* Printf.eprintf "read %d B (from fd %d)\n%!" len (Obj.magic fd); *)
- match Unix.read fd buf i len with
- | n_ ->
- n := n_;
- continue := false
- | exception
- Unix.Unix_error
- ( ( Unix.EBADF | Unix.ENOTCONN | Unix.ESHUTDOWN
- | Unix.ECONNRESET | Unix.EPIPE ),
- _,
- _ ) ->
- eof := true;
- continue := false
- | exception
- Unix.Unix_error
- ((Unix.EWOULDBLOCK | Unix.EAGAIN | Unix.EINTR), _, _) ->
- ignore (Unix.select [ fd ] [] [] 1.)
- done;
- (* Printf.eprintf "read returned %d B\n%!" !n; *)
- if !n = 0 then eof := true
- );
- !n);
- close =
- (fun () ->
- if not !closed then (
- closed := true;
- eof := true;
- if close_noerr then (
- try Unix.close fd with _ -> ()
- ) else
- Unix.close fd
- ));
- }
-
- let of_slice (i_bs : bytes) (i_off : int) (i_len : int) : t =
- let i_off = ref i_off in
- let i_len = ref i_len in
- {
- input =
- (fun buf i len ->
- let n = min len !i_len in
- Bytes.blit i_bs !i_off buf i n;
- i_off := !i_off + n;
- i_len := !i_len - n;
- n);
- close = ignore;
- }
-
- (** Read into the given slice.
- @return the number of bytes read, [0] means end of input. *)
- let[@inline] input (self : t) buf i len = self.input buf i len
-
- (** Read exactly [len] bytes.
- @raise End_of_file if the input did not contain enough data. *)
- let really_input (self : t) buf i len : unit =
- let i = ref i in
- let len = ref len in
- while !len > 0 do
- let n = input self buf !i !len in
- if n = 0 then raise End_of_file;
- i := !i + n;
- len := !len - n
- done
-
- (** Close the channel. *)
- let[@inline] close self : unit = self.close ()
-
- let append (i1 : t) (i2 : t) : t =
- let use_i1 = ref true in
- let rec input buf i len : int =
- if !use_i1 then (
- let n = i1.input buf i len in
- if n = 0 then (
- use_i1 := false;
- input buf i len
- ) else
- n
- ) else
- i2.input buf i len
- in
-
- {
- input;
- close =
- (fun () ->
- close i1;
- close i2);
- }
-end
-
-(** Output channel (byte sink) *)
-module Output = struct
- type t = {
- output_char: char -> unit; (** Output a single char *)
- output: bytes -> int -> int -> unit; (** Output slice *)
- flush: unit -> unit; (** Flush underlying buffer *)
- close: unit -> unit; (** Close the output. Must be idempotent. *)
- }
- (** An output channel, ie. a place into which we can write bytes.
-
- This can be a [Buffer.t], an [out_channel], a [Unix.file_descr], etc. *)
-
- let of_unix_fd ?(close_noerr = false) ~closed ~(buf : Buf.t)
- (fd : Unix.file_descr) : t =
- Buf.clear buf;
- let buf = Buf.bytes_slice buf in
- let off = ref 0 in
-
- let flush () =
- if !off > 0 then (
- let i = ref 0 in
- while !i < !off do
- (* Printf.eprintf "write %d bytes\n%!" (!off - !i); *)
- match Unix.write fd buf !i (!off - !i) with
- | 0 -> failwith "write failed"
- | n -> i := !i + n
- | exception
- Unix.Unix_error
- ( ( Unix.EBADF | Unix.ENOTCONN | Unix.ESHUTDOWN
- | Unix.ECONNRESET | Unix.EPIPE ),
- _,
- _ ) ->
- failwith "write failed"
- | exception
- Unix.Unix_error
- ((Unix.EWOULDBLOCK | Unix.EAGAIN | Unix.EINTR), _, _) ->
- ignore (Unix.select [] [ fd ] [] 1.)
- done;
- off := 0
- )
- in
-
- let[@inline] flush_if_full_ () = if !off = Bytes.length buf then flush () in
-
- let output_char c =
- flush_if_full_ ();
- Bytes.set buf !off c;
- incr off;
- flush_if_full_ ()
- in
- let output bs i len =
- (* Printf.eprintf "output %d bytes (buffered)\n%!" len; *)
- let i = ref i in
- let len = ref len in
- while !len > 0 do
- flush_if_full_ ();
- let n = min !len (Bytes.length buf - !off) in
- Bytes.blit bs !i buf !off n;
- i := !i + n;
- len := !len - n;
- off := !off + n
- done;
- flush_if_full_ ()
- in
- let close () =
- if not !closed then (
- closed := true;
- flush ();
- if close_noerr then (
- try Unix.close fd with _ -> ()
- ) else
- Unix.close fd
- )
- in
- { output; output_char; flush; close }
-
- (** [of_out_channel oc] wraps the channel into a {!Output.t}.
- @param close_noerr if true, then closing the result uses [close_out_noerr]
- instead of [close_out] to close [oc] *)
- let of_out_channel ?(close_noerr = false) (oc : out_channel) : t =
- {
- output_char = (fun c -> output_char oc c);
- output = (fun buf i len -> output oc buf i len);
- flush = (fun () -> flush oc);
- close =
- (fun () ->
- if close_noerr then
- close_out_noerr oc
- else
- close_out oc);
- }
-
- (** [of_buffer buf] is an output channel that writes directly into [buf].
- [flush] and [close] have no effect. *)
- let of_buffer (buf : Buffer.t) : t =
- {
- output_char = Buffer.add_char buf;
- output = Buffer.add_subbytes buf;
- flush = ignore;
- close = ignore;
- }
-
- (** Output the buffer slice into this channel *)
- let[@inline] output_char (self : t) c : unit = self.output_char c
-
- (** Output the buffer slice into this channel *)
- let[@inline] output (self : t) buf i len : unit = self.output buf i len
-
- let[@inline] output_string (self : t) (str : string) : unit =
- self.output (Bytes.unsafe_of_string str) 0 (String.length str)
-
- (** Close the channel. *)
- let[@inline] close self : unit = self.close ()
-
- (** Flush (ie. force write) any buffered bytes. *)
- let[@inline] flush self : unit = self.flush ()
-
- let output_buf (self : t) (buf : Buf.t) : unit =
- let b = Buf.bytes_slice buf in
- output self b 0 (Buf.size buf)
-
- (** [chunk_encoding oc] makes a new channel that outputs its content into [oc]
- in chunk encoding form.
- @param close_rec if true, closing the result will also close [oc]
- @param buf a buffer used to accumulate data into chunks.
- Chunks are emitted when [buf]'s size gets over a certain threshold,
- or when [flush] is called.
- *)
- let chunk_encoding ?(buf = Buf.create ()) ~close_rec (self : t) : t =
- (* write content of [buf] as a chunk if it's big enough.
- If [force=true] then write content of [buf] if it's simply non empty. *)
- let write_buf ~force () =
- let n = Buf.size buf in
- if (force && n > 0) || n >= 4_096 then (
- output_string self (Printf.sprintf "%x\r\n" n);
- self.output (Buf.bytes_slice buf) 0 n;
- output_string self "\r\n";
- Buf.clear buf
- )
- in
-
- let flush () =
- write_buf ~force:true ();
- self.flush ()
- in
-
- let close () =
- write_buf ~force:true ();
- (* write an empty chunk to close the stream *)
- output_string self "0\r\n";
- (* write another crlf after the stream (see #56) *)
- output_string self "\r\n";
- self.flush ();
- if close_rec then self.close ()
- in
- let output b i n =
- Buf.add_bytes buf b i n;
- write_buf ~force:false ()
- in
-
- let output_char c =
- Buf.add_char buf c;
- write_buf ~force:false ()
- in
- { output_char; flush; close; output }
-end
-
-(** A writer abstraction. *)
-module Writer = struct
- type t = { write: Output.t -> unit } [@@unboxed]
- (** Writer.
-
- A writer is a push-based stream of bytes.
- Give it an output channel and it will write the bytes in it.
-
- This is useful for responses: an http endpoint can return a writer
- as its response's body; the writer is given access to the connection
- to the client and can write into it as if it were a regular
- [out_channel], including controlling calls to [flush].
- Tiny_httpd will convert these writes into valid HTTP chunks.
- @since 0.14
- *)
-
- let[@inline] make ~write () : t = { write }
-
- (** Write into the channel. *)
- let[@inline] write (oc : Output.t) (self : t) : unit = self.write oc
-
- (** Empty writer, will output 0 bytes. *)
- let empty : t = { write = ignore }
-
- (** A writer that just emits the bytes from the given string. *)
- let[@inline] of_string (str : string) : t =
- let write oc = Output.output_string oc str in
- { write }
-end
-
-(** A TCP server abstraction. *)
-module TCP_server = struct
- type conn_handler = {
- handle: client_addr:Unix.sockaddr -> Input.t -> Output.t -> unit;
- (** Handle client connection *)
- }
-
- type t = {
- endpoint: unit -> string * int;
- (** Endpoint we listen on. This can only be called from within [serve]. *)
- active_connections: unit -> int;
- (** Number of connections currently active *)
- running: unit -> bool; (** Is the server currently running? *)
- stop: unit -> unit;
- (** Ask the server to stop. This might not take effect immediately,
- and is idempotent. After this [server.running()] must return [false]. *)
- }
- (** A running TCP server.
-
- This contains some functions that provide information about the running
- server, including whether it's active (as opposed to stopped), a function
- to stop it, and statistics about the number of connections. *)
-
- type builder = {
- serve: after_init:(t -> unit) -> handle:conn_handler -> unit -> unit;
- (** Blocking call to listen for incoming connections and handle them.
- Uses the connection handler [handle] to handle individual client
- connections in individual threads/fibers/tasks.
- @param after_init is called once with the server after the server
- has started. *)
- }
- (** A TCP server builder implementation.
-
- Calling [builder.serve ~after_init ~handle ()] starts a new TCP server on
- an unspecified endpoint
- (most likely coming from the function returning this builder)
- and returns the running server. *)
-end
diff --git a/src/Tiny_httpd_server.ml b/src/Tiny_httpd_server.ml
deleted file mode 100644
index f023324c..00000000
--- a/src/Tiny_httpd_server.ml
+++ /dev/null
@@ -1,1338 +0,0 @@
-type buf = Tiny_httpd_buf.t
-type byte_stream = Tiny_httpd_stream.t
-
-module Buf = Tiny_httpd_buf
-module Byte_stream = Tiny_httpd_stream
-module IO = Tiny_httpd_io
-module Pool = Tiny_httpd_pool
-module Log = Tiny_httpd_log
-
-exception Bad_req of int * string
-
-let bad_reqf c fmt = Printf.ksprintf (fun s -> raise (Bad_req (c, s))) fmt
-
-module Response_code = struct
- type t = int
-
- let ok = 200
- let not_found = 404
-
- let descr = function
- | 100 -> "Continue"
- | 200 -> "OK"
- | 201 -> "Created"
- | 202 -> "Accepted"
- | 204 -> "No content"
- | 300 -> "Multiple choices"
- | 301 -> "Moved permanently"
- | 302 -> "Found"
- | 304 -> "Not Modified"
- | 400 -> "Bad request"
- | 401 -> "Unauthorized"
- | 403 -> "Forbidden"
- | 404 -> "Not found"
- | 405 -> "Method not allowed"
- | 408 -> "Request timeout"
- | 409 -> "Conflict"
- | 410 -> "Gone"
- | 411 -> "Length required"
- | 413 -> "Payload too large"
- | 417 -> "Expectation failed"
- | 500 -> "Internal server error"
- | 501 -> "Not implemented"
- | 503 -> "Service unavailable"
- | n -> "Unknown response code " ^ string_of_int n (* TODO *)
-
- let[@inline] is_success n = n >= 200 && n < 400
-end
-
-type resp_error = Response_code.t * string
-type 'a resp_result = ('a, resp_error) result
-
-let unwrap_resp_result = function
- | Ok x -> x
- | Error (c, s) -> raise (Bad_req (c, s))
-
-module Meth = struct
- type t = [ `GET | `PUT | `POST | `HEAD | `DELETE | `OPTIONS ]
-
- let to_string = function
- | `GET -> "GET"
- | `PUT -> "PUT"
- | `HEAD -> "HEAD"
- | `POST -> "POST"
- | `DELETE -> "DELETE"
- | `OPTIONS -> "OPTIONS"
-
- let pp out s = Format.pp_print_string out (to_string s)
-
- let of_string = function
- | "GET" -> `GET
- | "PUT" -> `PUT
- | "POST" -> `POST
- | "HEAD" -> `HEAD
- | "DELETE" -> `DELETE
- | "OPTIONS" -> `OPTIONS
- | s -> bad_reqf 400 "unknown method %S" s
-end
-
-module Headers = struct
- type t = (string * string) list
-
- let empty = []
-
- let contains name headers =
- let name' = String.lowercase_ascii name in
- List.exists (fun (n, _) -> name' = n) headers
-
- let get_exn ?(f = fun x -> x) x h =
- let x' = String.lowercase_ascii x in
- List.assoc x' h |> f
-
- let get ?(f = fun x -> x) x h =
- try Some (get_exn ~f x h) with Not_found -> None
-
- let remove x h =
- let x' = String.lowercase_ascii x in
- List.filter (fun (k, _) -> k <> x') h
-
- let set x y h =
- let x' = String.lowercase_ascii x in
- (x', y) :: List.filter (fun (k, _) -> k <> x') h
-
- let pp out l =
- let pp_pair out (k, v) = Format.fprintf out "@[%s: %s@]" k v in
- Format.fprintf out "@[%a@]" (Format.pp_print_list pp_pair) l
-
- (* token = 1*tchar
- tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_"
- / "`" / "|" / "~" / DIGIT / ALPHA ; any VCHAR, except delimiters
- Reference: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 *)
- let is_tchar = function
- | '0' .. '9'
- | 'a' .. 'z'
- | 'A' .. 'Z'
- | '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_'
- | '`' | '|' | '~' ->
- true
- | _ -> false
-
- let for_all pred s =
- try
- String.iter (fun c -> if not (pred c) then raise Exit) s;
- true
- with Exit -> false
-
- let parse_ ~buf (bs : byte_stream) : t =
- let rec loop acc =
- let line = Byte_stream.read_line ~buf bs in
- Log.debug (fun k -> k "parsed header line %S" line);
- if line = "\r" then
- acc
- else (
- let k, v =
- try
- let i = String.index line ':' in
- let k = String.sub line 0 i in
- if not (for_all is_tchar k) then
- invalid_arg (Printf.sprintf "Invalid header key: %S" k);
- let v =
- String.sub line (i + 1) (String.length line - i - 1)
- |> String.trim
- in
- k, v
- with _ -> bad_reqf 400 "invalid header line: %S" line
- in
- loop ((String.lowercase_ascii k, v) :: acc)
- )
- in
- loop []
-end
-
-module Request = struct
- type 'body t = {
- meth: Meth.t;
- host: string;
- client_addr: Unix.sockaddr;
- headers: Headers.t;
- http_version: int * int;
- path: string;
- path_components: string list;
- query: (string * string) list;
- body: 'body;
- start_time: float;
- }
-
- let headers self = self.headers
- let host self = self.host
- let client_addr self = self.client_addr
- let meth self = self.meth
- let path self = self.path
- let body self = self.body
- let start_time self = self.start_time
- let query self = self.query
- let get_header ?f self h = Headers.get ?f h self.headers
-
- let remove_header k self =
- { self with headers = Headers.remove k self.headers }
-
- let get_header_int self h =
- match get_header self h with
- | Some x -> (try Some (int_of_string x) with _ -> None)
- | None -> None
-
- let set_header k v self = { self with headers = Headers.set k v self.headers }
- let update_headers f self = { self with headers = f self.headers }
- let set_body b self = { self with body = b }
-
- (** Should we close the connection after this request? *)
- let close_after_req (self : _ t) : bool =
- match self.http_version with
- | 1, 1 -> get_header self "connection" = Some "close"
- | 1, 0 -> not (get_header self "connection" = Some "keep-alive")
- | _ -> false
-
- let pp_comp_ out comp =
- Format.fprintf out "[%s]"
- (String.concat ";" @@ List.map (Printf.sprintf "%S") comp)
-
- let pp_query out q =
- Format.fprintf out "[%s]"
- (String.concat ";"
- @@ List.map (fun (a, b) -> Printf.sprintf "%S,%S" a b) q)
-
- let pp_ out self : unit =
- Format.fprintf out
- "{@[meth=%s;@ host=%s;@ headers=[@[%a@]];@ path=%S;@ body=?;@ \
- path_components=%a;@ query=%a@]}"
- (Meth.to_string self.meth) self.host Headers.pp self.headers self.path
- pp_comp_ self.path_components pp_query self.query
-
- let pp out self : unit =
- Format.fprintf out
- "{@[meth=%s;@ host=%s;@ headers=[@[%a@]];@ path=%S;@ body=%S;@ \
- path_components=%a;@ query=%a@]}"
- (Meth.to_string self.meth) self.host Headers.pp self.headers self.path
- self.body pp_comp_ self.path_components pp_query self.query
-
- (* decode a "chunked" stream into a normal stream *)
- let read_stream_chunked_ ?buf (bs : byte_stream) : byte_stream =
- Log.debug (fun k -> k "body: start reading chunked stream...");
- Byte_stream.read_chunked ?buf ~fail:(fun s -> Bad_req (400, s)) bs
-
- let limit_body_size_ ~max_size (bs : byte_stream) : byte_stream =
- Log.debug (fun k -> k "limit size of body to max-size=%d" max_size);
- Byte_stream.limit_size_to ~max_size ~close_rec:false bs
- ~too_big:(fun size ->
- (* read too much *)
- bad_reqf 413
- "body size was supposed to be %d, but at least %d bytes received"
- max_size size)
-
- let limit_body_size ~max_size (req : byte_stream t) : byte_stream t =
- { req with body = limit_body_size_ ~max_size req.body }
-
- (* read exactly [size] bytes from the stream *)
- let read_exactly ~size (bs : byte_stream) : byte_stream =
- Log.debug (fun k -> k "body: must read exactly %d bytes" size);
- Byte_stream.read_exactly bs ~close_rec:false ~size ~too_short:(fun size ->
- bad_reqf 400 "body is too short by %d bytes" size)
-
- (* parse request, but not body (yet) *)
- let parse_req_start ~client_addr ~get_time_s ~buf (bs : byte_stream) :
- unit t option resp_result =
- try
- let line = Byte_stream.read_line ~buf bs in
- let start_time = get_time_s () in
- let meth, path, version =
- try
- let off = ref 0 in
- let meth = Tiny_httpd_parse_.word line off in
- let path = Tiny_httpd_parse_.word line off in
- let http_version = Tiny_httpd_parse_.word line off in
- let version =
- match http_version with
- | "HTTP/1.1" -> 1
- | "HTTP/1.0" -> 0
- | v -> invalid_arg (Printf.sprintf "unsupported HTTP version: %s" v)
- in
- meth, path, version
- with
- | Invalid_argument msg ->
- Log.error (fun k -> k "invalid request line: `%s`: %s" line msg);
- raise (Bad_req (400, "Invalid request line"))
- | _ ->
- Log.error (fun k -> k "invalid request line: `%s`" line);
- raise (Bad_req (400, "Invalid request line"))
- in
- let meth = Meth.of_string meth in
- Log.debug (fun k -> k "got meth: %s, path %S" (Meth.to_string meth) path);
- let headers = Headers.parse_ ~buf bs in
- let host =
- match Headers.get "Host" headers with
- | None -> bad_reqf 400 "No 'Host' header in request"
- | Some h -> h
- in
- let path_components, query = Tiny_httpd_util.split_query path in
- let path_components = Tiny_httpd_util.split_on_slash path_components in
- let query =
- match Tiny_httpd_util.(parse_query query) with
- | Ok l -> l
- | Error e -> bad_reqf 400 "invalid query: %s" e
- in
- let req =
- {
- meth;
- query;
- host;
- client_addr;
- path;
- path_components;
- headers;
- http_version = 1, version;
- body = ();
- start_time;
- }
- in
- Ok (Some req)
- with
- | End_of_file | Sys_error _ | Unix.Unix_error _ -> Ok None
- | Bad_req (c, s) -> Error (c, s)
- | e -> Error (400, Printexc.to_string e)
-
- (* parse body, given the headers.
- @param tr_stream a transformation of the input stream. *)
- let parse_body_ ~tr_stream ~buf (req : byte_stream t) :
- byte_stream t resp_result =
- try
- let size =
- match Headers.get_exn "Content-Length" req.headers |> int_of_string with
- | n -> n (* body of fixed size *)
- | exception Not_found -> 0
- | exception _ -> bad_reqf 400 "invalid content-length"
- in
- let body =
- match get_header ~f:String.trim req "Transfer-Encoding" with
- | None -> read_exactly ~size @@ tr_stream req.body
- | Some "chunked" ->
- let bs =
- read_stream_chunked_ ~buf
- @@ tr_stream req.body (* body sent by chunks *)
- in
- if size > 0 then
- limit_body_size_ ~max_size:size bs
- else
- bs
- | Some s -> bad_reqf 500 "cannot handle transfer encoding: %s" s
- in
- Ok { req with body }
- with
- | End_of_file -> Error (400, "unexpected end of file")
- | Bad_req (c, s) -> Error (c, s)
- | e -> Error (400, Printexc.to_string e)
-
- let read_body_full ?buf ?buf_size (self : byte_stream t) : string t =
- try
- let buf =
- match buf with
- | Some b -> b
- | None -> Buf.create ?size:buf_size ()
- in
- let body = Byte_stream.read_all ~buf self.body in
- { self with body }
- with
- | Bad_req _ as e -> raise e
- | e -> bad_reqf 500 "failed to read body: %s" (Printexc.to_string e)
-
- module Internal_ = struct
- let parse_req_start ?(buf = Buf.create ()) ~client_addr ~get_time_s bs =
- parse_req_start ~client_addr ~get_time_s ~buf bs |> unwrap_resp_result
-
- let parse_body ?(buf = Buf.create ()) req bs : _ t =
- parse_body_ ~tr_stream:(fun s -> s) ~buf { req with body = bs }
- |> unwrap_resp_result
- end
-end
-
-module Response = struct
- type body =
- [ `String of string
- | `Stream of byte_stream
- | `Writer of IO.Writer.t
- | `Void ]
-
- type t = { code: Response_code.t; headers: Headers.t; body: body }
-
- let set_body body self = { self with body }
- let set_headers headers self = { self with headers }
- let update_headers f self = { self with headers = f self.headers }
- let set_header k v self = { self with headers = Headers.set k v self.headers }
-
- let remove_header k self =
- { self with headers = Headers.remove k self.headers }
-
- let set_code code self = { self with code }
-
- let make_raw ?(headers = []) ~code body : t =
- (* add content length to response *)
- let headers =
- Headers.set "Content-Length" (string_of_int (String.length body)) headers
- in
- { code; headers; body = `String body }
-
- let make_raw_stream ?(headers = []) ~code body : t =
- let headers = Headers.set "Transfer-Encoding" "chunked" headers in
- { code; headers; body = `Stream body }
-
- let make_raw_writer ?(headers = []) ~code body : t =
- let headers = Headers.set "Transfer-Encoding" "chunked" headers in
- { code; headers; body = `Writer body }
-
- let make_void_force_ ?(headers = []) ~code () : t =
- { code; headers; body = `Void }
-
- let make_void ?(headers = []) ~code () : t =
- let is_ok = code < 200 || code = 204 || code = 304 in
- if is_ok then
- make_void_force_ ~headers ~code ()
- else
- make_raw ~headers ~code "" (* invalid to not have a body *)
-
- let make_string ?headers ?(code = 200) r =
- match r with
- | Ok body -> make_raw ?headers ~code body
- | Error (code, msg) -> make_raw ?headers ~code msg
-
- let make_stream ?headers ?(code = 200) r =
- match r with
- | Ok body -> make_raw_stream ?headers ~code body
- | Error (code, msg) -> make_raw ?headers ~code msg
-
- let make_writer ?headers ?(code = 200) r : t =
- match r with
- | Ok body -> make_raw_writer ?headers ~code body
- | Error (code, msg) -> make_raw ?headers ~code msg
-
- let make ?headers ?(code = 200) r : t =
- match r with
- | Ok (`String body) -> make_raw ?headers ~code body
- | Ok (`Stream body) -> make_raw_stream ?headers ~code body
- | Ok `Void -> make_void ?headers ~code ()
- | Ok (`Writer f) -> make_raw_writer ?headers ~code f
- | Error (code, msg) -> make_raw ?headers ~code msg
-
- let fail ?headers ~code fmt =
- Printf.ksprintf (fun msg -> make_raw ?headers ~code msg) fmt
-
- let fail_raise ~code fmt =
- Printf.ksprintf (fun msg -> raise (Bad_req (code, msg))) fmt
-
- let pp out self : unit =
- let pp_body out = function
- | `String s -> Format.fprintf out "%S" s
- | `Stream _ -> Format.pp_print_string out ""
- | `Writer _ -> Format.pp_print_string out ""
- | `Void -> ()
- in
- Format.fprintf out "{@[code=%d;@ headers=[@[%a@]];@ body=%a@]}" self.code
- Headers.pp self.headers pp_body self.body
-
- let output_ ~buf (oc : IO.Output.t) (self : t) : unit =
- (* double indirection:
- - print into [buffer] using [bprintf]
- - transfer to [buf_] so we can output from there *)
- let tmp_buffer = Buffer.create 32 in
- Buf.clear buf;
-
- (* write start of reply *)
- Printf.bprintf tmp_buffer "HTTP/1.1 %d %s\r\n" self.code
- (Response_code.descr self.code);
- Buf.add_buffer buf tmp_buffer;
- Buffer.clear tmp_buffer;
-
- let body, is_chunked =
- match self.body with
- | `String s when String.length s > 1024 * 500 ->
- (* chunk-encode large bodies *)
- `Writer (IO.Writer.of_string s), true
- | `String _ as b -> b, false
- | `Stream _ as b -> b, true
- | `Writer _ as b -> b, true
- | `Void as b -> b, false
- in
- let headers =
- if is_chunked then
- self.headers
- |> Headers.set "transfer-encoding" "chunked"
- |> Headers.remove "content-length"
- else
- self.headers
- in
- let self = { self with headers; body } in
- Log.debug (fun k ->
- k "t[%d]: output response: %s"
- (Thread.id @@ Thread.self ())
- (Format.asprintf "%a" pp { self with body = `String "<...>" }));
-
- (* write headers, using [buf] to batch writes *)
- List.iter
- (fun (k, v) ->
- Printf.bprintf tmp_buffer "%s: %s\r\n" k v;
- Buf.add_buffer buf tmp_buffer;
- Buffer.clear tmp_buffer)
- headers;
-
- IO.Output.output_buf oc buf;
- IO.Output.output_string oc "\r\n";
- Buf.clear buf;
-
- (match body with
- | `String "" | `Void -> ()
- | `String s -> IO.Output.output_string oc s
- | `Writer w ->
- (* use buffer to chunk encode [w] *)
- let oc' = IO.Output.chunk_encoding ~buf ~close_rec:false oc in
- (try
- IO.Writer.write oc' w;
- IO.Output.close oc'
- with e ->
- let bt = Printexc.get_raw_backtrace () in
- IO.Output.close oc';
- IO.Output.flush oc;
- Printexc.raise_with_backtrace e bt)
- | `Stream str ->
- (match Byte_stream.output_chunked' ~buf oc str with
- | () ->
- Log.debug (fun k ->
- k "t[%d]: done outputing stream" (Thread.id @@ Thread.self ()));
- Byte_stream.close str
- | exception e ->
- let bt = Printexc.get_raw_backtrace () in
- Log.error (fun k ->
- k "t[%d]: outputing stream failed with %s"
- (Thread.id @@ Thread.self ())
- (Printexc.to_string e));
- Byte_stream.close str;
- IO.Output.flush oc;
- Printexc.raise_with_backtrace e bt));
- IO.Output.flush oc
-end
-
-(* semaphore, for limiting concurrency. *)
-module Sem_ = struct
- type t = { mutable n: int; max: int; mutex: Mutex.t; cond: Condition.t }
-
- let create n =
- if n <= 0 then invalid_arg "Semaphore.create";
- { n; max = n; mutex = Mutex.create (); cond = Condition.create () }
-
- let acquire m t =
- Mutex.lock t.mutex;
- while t.n < m do
- Condition.wait t.cond t.mutex
- done;
- assert (t.n >= m);
- t.n <- t.n - m;
- Condition.broadcast t.cond;
- Mutex.unlock t.mutex
-
- let release m t =
- Mutex.lock t.mutex;
- t.n <- t.n + m;
- Condition.broadcast t.cond;
- Mutex.unlock t.mutex
-
- let num_acquired t = t.max - t.n
-end
-
-module Route = struct
- type path = string list (* split on '/' *)
-
- type (_, _) comp =
- | Exact : string -> ('a, 'a) comp
- | Int : (int -> 'a, 'a) comp
- | String : (string -> 'a, 'a) comp
- | String_urlencoded : (string -> 'a, 'a) comp
-
- type (_, _) t =
- | Fire : ('b, 'b) t
- | Rest : { url_encoded: bool } -> (string -> 'b, 'b) t
- | Compose : ('a, 'b) comp * ('b, 'c) t -> ('a, 'c) t
-
- let return = Fire
- let rest_of_path = Rest { url_encoded = false }
- let rest_of_path_urlencoded = Rest { url_encoded = true }
- let ( @/ ) a b = Compose (a, b)
- let string = String
- let string_urlencoded = String_urlencoded
- let int = Int
- let exact (s : string) = Exact s
-
- let exact_path (s : string) tail =
- let rec fn = function
- | [] -> tail
- | "" :: ls -> fn ls
- | s :: ls -> exact s @/ fn ls
- in
- fn (String.split_on_char '/' s)
-
- let rec eval : type a b. path -> (a, b) t -> a -> b option =
- fun path route f ->
- match path, route with
- | [], Fire -> Some f
- | _, Fire -> None
- | _, Rest { url_encoded } ->
- let whole_path = String.concat "/" path in
- (match
- if url_encoded then (
- match Tiny_httpd_util.percent_decode whole_path with
- | Some s -> s
- | None -> raise_notrace Exit
- ) else
- whole_path
- with
- | whole_path -> Some (f whole_path)
- | exception Exit -> None)
- | c1 :: path', Compose (comp, route') ->
- (match comp with
- | Int ->
- (match int_of_string c1 with
- | i -> eval path' route' (f i)
- | exception _ -> None)
- | String -> eval path' route' (f c1)
- | String_urlencoded ->
- (match Tiny_httpd_util.percent_decode c1 with
- | None -> None
- | Some s -> eval path' route' (f s))
- | Exact s ->
- if s = c1 then
- eval path' route' f
- else
- None)
- | [], Compose (String, Fire) -> Some (f "") (* trailing *)
- | [], Compose (String_urlencoded, Fire) -> Some (f "") (* trailing *)
- | [], Compose _ -> None
-
- let bpf = Printf.bprintf
-
- let rec pp_ : type a b. Buffer.t -> (a, b) t -> unit =
- fun out -> function
- | Fire -> bpf out "/"
- | Rest { url_encoded } ->
- bpf out ""
- (if url_encoded then
- "_urlencoded"
- else
- "")
- | Compose (Exact s, tl) -> bpf out "%s/%a" s pp_ tl
- | Compose (Int, tl) -> bpf out "/%a" pp_ tl
- | Compose (String, tl) -> bpf out "/%a" pp_ tl
- | Compose (String_urlencoded, tl) -> bpf out "/%a" pp_ tl
-
- let to_string x =
- let b = Buffer.create 16 in
- pp_ b x;
- Buffer.contents b
-
- let pp out x = Format.pp_print_string out (to_string x)
-end
-
-module Middleware = struct
- type handler = byte_stream Request.t -> resp:(Response.t -> unit) -> unit
- type t = handler -> handler
-
- (** Apply a list of middlewares to [h] *)
- let apply_l (l : t list) (h : handler) : handler =
- List.fold_right (fun m h -> m h) l h
-
- let[@inline] nil : t = fun h -> h
-end
-
-(* a request handler. handles a single request. *)
-type cb_path_handler = IO.Output.t -> Middleware.handler
-
-module type SERVER_SENT_GENERATOR = sig
- val set_headers : Headers.t -> unit
-
- val send_event :
- ?event:string -> ?id:string -> ?retry:string -> data:string -> unit -> unit
-
- val close : unit -> unit
-end
-
-type server_sent_generator = (module SERVER_SENT_GENERATOR)
-
-(** Handler that upgrades to another protocol *)
-module type UPGRADE_HANDLER = sig
- type handshake_state
- (** Some specific state returned after handshake *)
-
- val name : string
- (** Name in the "upgrade" header *)
-
- val handshake : unit Request.t -> (Headers.t * handshake_state, string) result
- (** Perform the handshake and upgrade the connection. The returned
- code is [101] alongside these headers. *)
-
- val handle_connection :
- Unix.sockaddr -> handshake_state -> IO.Input.t -> IO.Output.t -> unit
- (** Take control of the connection and take it from there *)
-end
-
-type upgrade_handler = (module UPGRADE_HANDLER)
-
-exception Upgrade of unit Request.t * upgrade_handler
-
-module type IO_BACKEND = sig
- val init_addr : unit -> string
- val init_port : unit -> int
-
- val get_time_s : unit -> float
- (** obtain the current timestamp in seconds. *)
-
- val tcp_server : unit -> Tiny_httpd_io.TCP_server.builder
- (** Server that can listen on a port and handle clients. *)
-end
-
-type handler_result =
- | Handle of cb_path_handler
- | Fail of resp_error
- | Upgrade of upgrade_handler
-
-let unwrap_handler_result req = function
- | Handle x -> x
- | Fail (c, s) -> raise (Bad_req (c, s))
- | Upgrade up -> raise (Upgrade (req, up))
-
-type t = {
- backend: (module IO_BACKEND);
- mutable tcp_server: IO.TCP_server.t option;
- buf_size: int;
- mutable handler: byte_stream Request.t -> Response.t;
- (** toplevel handler, if any *)
- mutable middlewares: (int * Middleware.t) list; (** Global middlewares *)
- mutable middlewares_sorted: (int * Middleware.t) list lazy_t;
- (** sorted version of {!middlewares} *)
- mutable path_handlers: (unit Request.t -> handler_result option) list;
- (** path handlers *)
- buf_pool: Buf.t Pool.t;
-}
-
-let get_addr_ sock =
- match Unix.getsockname sock with
- | Unix.ADDR_INET (addr, port) -> addr, port
- | _ -> invalid_arg "httpd: address is not INET"
-
-let addr (self : t) =
- match self.tcp_server with
- | None ->
- let (module B) = self.backend in
- B.init_addr ()
- | Some s -> fst @@ s.endpoint ()
-
-let port (self : t) =
- match self.tcp_server with
- | None ->
- let (module B) = self.backend in
- B.init_port ()
- | Some s -> snd @@ s.endpoint ()
-
-let active_connections (self : t) =
- match self.tcp_server with
- | None -> 0
- | Some s -> s.active_connections ()
-
-let add_middleware ~stage self m =
- let stage =
- match stage with
- | `Encoding -> 0
- | `Stage n when n < 1 -> invalid_arg "add_middleware: bad stage"
- | `Stage n -> n
- in
- self.middlewares <- (stage, m) :: self.middlewares;
- self.middlewares_sorted <-
- lazy
- (List.stable_sort (fun (s1, _) (s2, _) -> compare s1 s2) self.middlewares)
-
-let add_decode_request_cb self f =
- (* turn it into a middleware *)
- let m h req ~resp =
- (* see if [f] modifies the stream *)
- let req0 = { req with Request.body = () } in
- match f req0 with
- | None -> h req ~resp (* pass through *)
- | Some (req1, tr_stream) ->
- let req = { req1 with Request.body = tr_stream req.Request.body } in
- h req ~resp
- in
- add_middleware self ~stage:`Encoding m
-
-let add_encode_response_cb self f =
- let m h req ~resp =
- h req ~resp:(fun r ->
- let req0 = { req with Request.body = () } in
- (* now transform [r] if we want to *)
- match f req0 r with
- | None -> resp r
- | Some r' -> resp r')
- in
- add_middleware self ~stage:`Encoding m
-
-let set_top_handler self f = self.handler <- f
-
-(* route the given handler.
- @param tr_req wraps the actual concrete function returned by the route
- and makes it into a handler. *)
-let add_route_handler_ ?(accept = fun _req -> Ok ()) ?(middlewares = []) ?meth
- ~tr_req self (route : _ Route.t) f =
- let ph req : handler_result option =
- match meth with
- | Some m when m <> req.Request.meth -> None (* ignore *)
- | _ ->
- (match Route.eval req.Request.path_components route f with
- | Some handler ->
- (* we have a handler, do we accept the request based on its headers? *)
- (match accept req with
- | Ok () ->
- Some
- (Handle
- (fun oc ->
- Middleware.apply_l middlewares @@ fun req ~resp ->
- tr_req oc req ~resp handler))
- | Error err -> Some (Fail err))
- | None -> None (* path didn't match *))
- in
- self.path_handlers <- ph :: self.path_handlers
-
-let add_route_handler (type a) ?accept ?middlewares ?meth self
- (route : (a, _) Route.t) (f : _) : unit =
- let tr_req _oc req ~resp f =
- let req =
- Pool.with_resource self.buf_pool @@ fun buf ->
- Request.read_body_full ~buf req
- in
- resp (f req)
- in
- add_route_handler_ ?accept ?middlewares ?meth self route ~tr_req f
-
-let add_route_handler_stream ?accept ?middlewares ?meth self route f =
- let tr_req _oc req ~resp f = resp (f req) in
- add_route_handler_ ?accept ?middlewares ?meth self route ~tr_req f
-
-let[@inline] _opt_iter ~f o =
- match o with
- | None -> ()
- | Some x -> f x
-
-exception Exit_SSE
-
-let add_route_server_sent_handler ?accept self route f =
- let tr_req (oc : IO.Output.t) req ~resp f =
- let req =
- Pool.with_resource self.buf_pool @@ fun buf ->
- Request.read_body_full ~buf req
- in
- let headers =
- ref Headers.(empty |> set "content-type" "text/event-stream")
- in
-
- (* send response once *)
- let resp_sent = ref false in
- let send_response_idempotent_ () =
- if not !resp_sent then (
- resp_sent := true;
- (* send 200 response now *)
- let initial_resp =
- Response.make_void_force_ ~headers:!headers ~code:200 ()
- in
- resp initial_resp
- )
- in
-
- let[@inline] writef fmt =
- Printf.ksprintf (IO.Output.output_string oc) fmt
- in
-
- let send_event ?event ?id ?retry ~data () : unit =
- send_response_idempotent_ ();
- _opt_iter event ~f:(fun e -> writef "event: %s\n" e);
- _opt_iter id ~f:(fun e -> writef "id: %s\n" e);
- _opt_iter retry ~f:(fun e -> writef "retry: %s\n" e);
- let l = String.split_on_char '\n' data in
- List.iter (fun s -> writef "data: %s\n" s) l;
- IO.Output.output_string oc "\n";
- (* finish group *)
- IO.Output.flush oc
- in
- let module SSG = struct
- let set_headers h =
- if not !resp_sent then (
- headers := List.rev_append h !headers;
- send_response_idempotent_ ()
- )
-
- let send_event = send_event
- let close () = raise Exit_SSE
- end in
- (try f req (module SSG : SERVER_SENT_GENERATOR)
- with Exit_SSE -> IO.Output.close oc);
- Log.info (fun k -> k "closed SSE connection")
- in
- add_route_handler_ self ?accept ~meth:`GET route ~tr_req f
-
-let add_upgrade_handler ?(accept = fun _ -> Ok ()) (self : t) route f : unit =
- let ph req : handler_result option =
- if req.Request.meth <> `GET then
- None
- else (
- match accept req with
- | Ok () ->
- (match Route.eval req.Request.path_components route f with
- | Some up -> Some (Upgrade up)
- | None -> None (* path didn't match *))
- | Error err -> Some (Fail err)
- )
- in
- self.path_handlers <- ph :: self.path_handlers
-
-let get_max_connection_ ?(max_connections = 64) () : int =
- let max_connections = max 4 max_connections in
- max_connections
-
-let create_from ?(buf_size = 16 * 1_024) ?(middlewares = []) ~backend () : t =
- let handler _req = Response.fail ~code:404 "no top handler" in
- let self =
- {
- backend;
- tcp_server = None;
- handler;
- buf_size;
- path_handlers = [];
- middlewares = [];
- middlewares_sorted = lazy [];
- buf_pool =
- Pool.create ~clear:Buf.clear_and_zero
- ~mk_item:(fun () -> Buf.create ~size:buf_size ())
- ();
- }
- in
- List.iter (fun (stage, m) -> add_middleware self ~stage m) middlewares;
- self
-
-let is_ipv6_str addr : bool = String.contains addr ':'
-
-module Unix_tcp_server_ = struct
- type t = {
- addr: string;
- port: int;
- buf_pool: Buf.t Pool.t;
- max_connections: int;
- sem_max_connections: Sem_.t;
- (** semaphore to restrict the number of active concurrent connections *)
- mutable sock: Unix.file_descr option; (** Socket *)
- new_thread: (unit -> unit) -> unit;
- timeout: float;
- masksigpipe: bool;
- mutable running: bool; (* TODO: use an atomic? *)
- }
-
- let shutdown_silent_ fd =
- try Unix.shutdown fd Unix.SHUTDOWN_ALL with _ -> ()
-
- let close_silent_ fd = try Unix.close fd with _ -> ()
-
- let to_tcp_server (self : t) : IO.TCP_server.builder =
- {
- IO.TCP_server.serve =
- (fun ~after_init ~handle () : unit ->
- if self.masksigpipe then
- ignore (Unix.sigprocmask Unix.SIG_BLOCK [ Sys.sigpipe ] : _ list);
- let sock, should_bind =
- match self.sock with
- | Some s ->
- ( s,
- false
- (* Because we're getting a socket from the caller (e.g. systemd) *)
- )
- | None ->
- ( Unix.socket
- (if is_ipv6_str self.addr then
- Unix.PF_INET6
- else
- Unix.PF_INET)
- Unix.SOCK_STREAM 0,
- true (* Because we're creating the socket ourselves *) )
- in
- Unix.clear_nonblock sock;
- Unix.setsockopt_optint sock Unix.SO_LINGER None;
- if should_bind then (
- let inet_addr = Unix.inet_addr_of_string self.addr in
- Unix.setsockopt sock Unix.SO_REUSEADDR true;
- Unix.bind sock (Unix.ADDR_INET (inet_addr, self.port));
- let n_listen = 2 * self.max_connections in
- Unix.listen sock n_listen
- );
-
- self.sock <- Some sock;
-
- let tcp_server =
- {
- IO.TCP_server.stop = (fun () -> self.running <- false);
- running = (fun () -> self.running);
- active_connections =
- (fun () -> Sem_.num_acquired self.sem_max_connections - 1);
- endpoint =
- (fun () ->
- let addr, port = get_addr_ sock in
- Unix.string_of_inet_addr addr, port);
- }
- in
- after_init tcp_server;
-
- (* how to handle a single client *)
- let handle_client_unix_ (client_sock : Unix.file_descr)
- (client_addr : Unix.sockaddr) : unit =
- Log.info (fun k ->
- k "t[%d]: serving new client on %s"
- (Thread.id @@ Thread.self ())
- (Tiny_httpd_util.show_sockaddr client_addr));
-
- if self.masksigpipe then
- ignore (Unix.sigprocmask Unix.SIG_BLOCK [ Sys.sigpipe ] : _ list);
- Unix.set_nonblock client_sock;
- Unix.setsockopt client_sock Unix.TCP_NODELAY true;
- Unix.(setsockopt_float client_sock SO_RCVTIMEO self.timeout);
- Unix.(setsockopt_float client_sock SO_SNDTIMEO self.timeout);
- Pool.with_resource self.buf_pool @@ fun buf ->
- let closed = ref false in
- let oc =
- IO.Output.of_unix_fd ~close_noerr:true ~closed ~buf client_sock
- in
- let ic =
- IO.Input.of_unix_fd ~close_noerr:true ~closed client_sock
- in
- handle.handle ~client_addr ic oc
- in
-
- Unix.set_nonblock sock;
- while self.running do
- match Unix.accept sock with
- | client_sock, client_addr ->
- (* limit concurrency *)
- Sem_.acquire 1 self.sem_max_connections;
- (* Block INT/HUP while cloning to avoid children handling them.
- When thread gets them, our Unix.accept raises neatly. *)
- ignore Unix.(sigprocmask SIG_BLOCK Sys.[ sigint; sighup ]);
- self.new_thread (fun () ->
- try
- handle_client_unix_ client_sock client_addr;
- Log.info (fun k ->
- k "t[%d]: done with client on %s, exiting"
- (Thread.id @@ Thread.self ())
- @@ Tiny_httpd_util.show_sockaddr client_addr);
- shutdown_silent_ client_sock;
- close_silent_ client_sock;
- Sem_.release 1 self.sem_max_connections
- with e ->
- let bt = Printexc.get_raw_backtrace () in
- shutdown_silent_ client_sock;
- close_silent_ client_sock;
- Sem_.release 1 self.sem_max_connections;
- Log.error (fun k ->
- k
- "@[Handler: uncaught exception for client %s:@ \
- %s@ %s@]"
- (Tiny_httpd_util.show_sockaddr client_addr)
- (Printexc.to_string e)
- (Printexc.raw_backtrace_to_string bt)));
- ignore Unix.(sigprocmask SIG_UNBLOCK Sys.[ sigint; sighup ])
- | exception Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK), _, _)
- ->
- (* wait for the socket to be ready, and re-enter the loop *)
- ignore (Unix.select [ sock ] [] [ sock ] 1.0 : _ * _ * _)
- | exception e ->
- Log.error (fun k ->
- k "Unix.accept raised an exception: %s" (Printexc.to_string e));
- Thread.delay 0.01
- done;
-
- (* Wait for all threads to be done: this only works if all threads are done. *)
- Unix.close sock;
- Sem_.acquire self.sem_max_connections.max self.sem_max_connections;
- ());
- }
-end
-
-let create ?(masksigpipe = true) ?max_connections ?(timeout = 0.0) ?buf_size
- ?(get_time_s = Unix.gettimeofday)
- ?(new_thread = fun f -> ignore (Thread.create f () : Thread.t))
- ?(addr = "127.0.0.1") ?(port = 8080) ?sock ?middlewares () : t =
- let max_connections = get_max_connection_ ?max_connections () in
- let server =
- {
- Unix_tcp_server_.addr;
- new_thread;
- buf_pool =
- Pool.create ~clear:Buf.clear_and_zero
- ~mk_item:(fun () -> Buf.create ?size:buf_size ())
- ();
- running = true;
- port;
- sock;
- max_connections;
- sem_max_connections = Sem_.create max_connections;
- masksigpipe;
- timeout;
- }
- in
- let tcp_server_builder = Unix_tcp_server_.to_tcp_server server in
- let module B = struct
- let init_addr () = addr
- let init_port () = port
- let get_time_s = get_time_s
- let tcp_server () = tcp_server_builder
- end in
- let backend = (module B : IO_BACKEND) in
- create_from ?buf_size ?middlewares ~backend ()
-
-let stop (self : t) =
- match self.tcp_server with
- | None -> ()
- | Some s -> s.stop ()
-
-let running (self : t) =
- match self.tcp_server with
- | None -> false
- | Some s -> s.running ()
-
-let find_map f l =
- let rec aux f = function
- | [] -> None
- | x :: l' ->
- (match f x with
- | Some _ as res -> res
- | None -> aux f l')
- in
- aux f l
-
-let string_as_list_contains_ (s : string) (sub : string) : bool =
- let fragments = String.split_on_char ',' s in
- List.exists (fun fragment -> String.trim fragment = sub) fragments
-
-(* handle client on [ic] and [oc] *)
-let client_handle_for (self : t) ~client_addr ic oc : unit =
- Pool.with_resource self.buf_pool @@ fun buf ->
- Pool.with_resource self.buf_pool @@ fun buf_res ->
- let is = Byte_stream.of_input ~buf_size:self.buf_size ic in
- let (module B) = self.backend in
-
- (* how to log the response to this query *)
- let log_response (req : _ Request.t) (resp : Response.t) =
- if not Log.dummy then (
- let msgf k =
- let elapsed = B.get_time_s () -. req.start_time in
- k
- ("response to=%s code=%d time=%.3fs path=%S" : _ format4)
- (Tiny_httpd_util.show_sockaddr client_addr)
- resp.code elapsed req.path
- in
- if Response_code.is_success resp.code then
- Log.info msgf
- else
- Log.error msgf
- )
- in
-
- let log_exn msg bt =
- Log.error (fun k ->
- k "error while processing response for %s msg=%s@.%s"
- (Tiny_httpd_util.show_sockaddr client_addr)
- msg
- (Printexc.raw_backtrace_to_string bt))
- in
-
- (* handle generic exception *)
- let handle_exn e bt : unit =
- let msg = Printexc.to_string e in
- let resp = Response.fail ~code:500 "server error: %s" msg in
- if not Log.dummy then log_exn msg bt;
- Response.output_ ~buf:buf_res oc resp
- in
-
- let handle_bad_req req e bt =
- let msg = Printexc.to_string e in
- let resp = Response.fail ~code:500 "server error: %s" msg in
- if not Log.dummy then (
- log_exn msg bt;
- log_response req resp
- );
- Response.output_ ~buf:buf_res oc resp
- in
-
- let handle_upgrade req (module UP : UPGRADE_HANDLER) : unit =
- Log.debug (fun k -> k "upgrade connection");
- try
- (* check headers *)
- (match Request.get_header req "connection" with
- | Some str when string_as_list_contains_ str "Upgrade" -> ()
- | _ -> bad_reqf 426 "connection header must contain 'Upgrade'");
- (match Request.get_header req "upgrade" with
- | Some u when u = UP.name -> ()
- | Some u -> bad_reqf 426 "expected upgrade to be '%s', got '%s'" UP.name u
- | None -> bad_reqf 426 "expected 'connection: upgrade' header");
-
- (* ok, this is the upgrade we expected *)
- match UP.handshake req with
- | Error msg ->
- (* fail the upgrade *)
- Log.error (fun k -> k "upgrade failed: %s" msg);
- let resp = Response.make_raw ~code:429 "upgrade required" in
- log_response req resp;
- Response.output_ ~buf:buf_res oc resp
- | Ok (headers, handshake_st) ->
- (* send the upgrade reply *)
- let headers =
- [ "connection", "upgrade"; "upgrade", UP.name ] @ headers
- in
- let resp = Response.make_string ~code:101 ~headers (Ok "") in
- log_response req resp;
- Response.output_ ~buf:buf_res oc resp;
-
- (* now, give the whole connection over to the upgraded connection.
- Make sure to give the leftovers from [is] as well, if any.
- There might not be any because the first message doesn't normally come
- directly in the same packet as the handshake, but still. *)
- let ic =
- if is.len > 0 then (
- Log.debug (fun k -> k "LEFTOVERS! %d B" is.len);
- IO.Input.append (IO.Input.of_slice is.bs is.off is.len) ic
- ) else
- ic
- in
-
- UP.handle_connection client_addr handshake_st ic oc
- with e ->
- let bt = Printexc.get_raw_backtrace () in
- handle_bad_req req e bt
- in
-
- let continue = ref true in
-
- let handle_one_req () =
- match
- Request.parse_req_start ~client_addr ~get_time_s:B.get_time_s ~buf is
- with
- | Ok None -> continue := false (* client is done *)
- | Error (c, s) ->
- (* connection error, close *)
- let res = Response.make_raw ~code:c s in
- (try Response.output_ ~buf:buf_res oc res with Sys_error _ -> ());
- continue := false
- | Ok (Some req) ->
- Log.debug (fun k ->
- k "t[%d]: parsed request: %s"
- (Thread.id @@ Thread.self ())
- (Format.asprintf "@[%a@]" Request.pp_ req));
-
- if Request.close_after_req req then continue := false;
-
- (try
- (* is there a handler for this path? *)
- let base_handler =
- match find_map (fun ph -> ph req) self.path_handlers with
- | Some f -> unwrap_handler_result req f
- | None -> fun _oc req ~resp -> resp (self.handler req)
- in
-
- (* handle expect/continue *)
- (match Request.get_header ~f:String.trim req "Expect" with
- | Some "100-continue" ->
- Log.debug (fun k -> k "send back: 100 CONTINUE");
- Response.output_ ~buf:buf_res oc (Response.make_raw ~code:100 "")
- | Some s -> bad_reqf 417 "unknown expectation %s" s
- | None -> ());
-
- (* apply middlewares *)
- let handler oc =
- List.fold_right
- (fun (_, m) h -> m h)
- (Lazy.force self.middlewares_sorted)
- (base_handler oc)
- in
-
- (* now actually read request's body into a stream *)
- let req =
- Request.parse_body_
- ~tr_stream:(fun s -> s)
- ~buf { req with body = is }
- |> unwrap_resp_result
- in
-
- (* how to reply *)
- let resp r =
- try
- if Headers.get "connection" r.Response.headers = Some "close" then
- continue := false;
- log_response req r;
- Response.output_ ~buf:buf_res oc r
- with Sys_error e ->
- Log.debug (fun k ->
- k "error when writing response: %s@.connection broken" e);
- continue := false
- in
-
- (* call handler *)
- try handler oc req ~resp
- with Sys_error e ->
- Log.debug (fun k ->
- k "error while handling request: %s@.connection broken" e);
- continue := false
- with
- | Sys_error e ->
- (* connection broken somehow *)
- Log.debug (fun k -> k "error: %s@. connection broken" e);
- continue := false
- | Bad_req (code, s) ->
- continue := false;
- let resp = Response.make_raw ~code s in
- log_response req resp;
- Response.output_ ~buf:buf_res oc resp
- | Upgrade _ as e -> raise e
- | e ->
- let bt = Printexc.get_raw_backtrace () in
- handle_bad_req req e bt)
- in
-
- try
- while !continue && running self do
- Log.debug (fun k ->
- k "t[%d]: read next request" (Thread.id @@ Thread.self ()));
- handle_one_req ()
- done
- with
- | Upgrade (req, up) ->
- (* upgrades take over the whole connection, we won't process
- any further request *)
- handle_upgrade req up
- | e ->
- let bt = Printexc.get_raw_backtrace () in
- handle_exn e bt
-
-let client_handler (self : t) : IO.TCP_server.conn_handler =
- { IO.TCP_server.handle = client_handle_for self }
-
-let is_ipv6 (self : t) =
- let (module B) = self.backend in
- is_ipv6_str (B.init_addr ())
-
-let run_exn ?(after_init = ignore) (self : t) : unit =
- let (module B) = self.backend in
- let server = B.tcp_server () in
- server.serve
- ~after_init:(fun tcp_server ->
- self.tcp_server <- Some tcp_server;
- after_init ())
- ~handle:(client_handler self) ()
-
-let run ?after_init self : _ result =
- try Ok (run_exn ?after_init self) with e -> Error e
diff --git a/src/Tiny_httpd_server.mli b/src/Tiny_httpd_server.mli
deleted file mode 100644
index c9ee0763..00000000
--- a/src/Tiny_httpd_server.mli
+++ /dev/null
@@ -1,719 +0,0 @@
-(** HTTP server.
-
- This module implements a very simple, basic HTTP/1.1 server using blocking
- IOs and threads.
-
- It is possible to use a thread pool, see {!create}'s argument [new_thread].
-
- @since 0.13
-*)
-
-type buf = Tiny_httpd_buf.t
-type byte_stream = Tiny_httpd_stream.t
-
-(** {2 HTTP Methods} *)
-
-module Meth : sig
- type t = [ `GET | `PUT | `POST | `HEAD | `DELETE | `OPTIONS ]
- (** A HTTP method.
- For now we only handle a subset of these.
-
- See https://tools.ietf.org/html/rfc7231#section-4 *)
-
- val pp : Format.formatter -> t -> unit
- val to_string : t -> string
-end
-
-(** {2 Headers}
-
- Headers are metadata associated with a request or response. *)
-
-module Headers : sig
- type t = (string * string) list
- (** The header files of a request or response.
-
- Neither the key nor the value can contain ['\r'] or ['\n'].
- See https://tools.ietf.org/html/rfc7230#section-3.2 *)
-
- val empty : t
- (** Empty list of headers.
- @since 0.5 *)
-
- val get : ?f:(string -> string) -> string -> t -> string option
- (** [get k headers] looks for the header field with key [k].
- @param f if provided, will transform the value before it is returned. *)
-
- val set : string -> string -> t -> t
- (** [set k v headers] sets the key [k] to value [v].
- It erases any previous entry for [k] *)
-
- val remove : string -> t -> t
- (** Remove the key from the headers, if present. *)
-
- val contains : string -> t -> bool
- (** Is there a header with the given key? *)
-
- val pp : Format.formatter -> t -> unit
- (** Pretty print the headers. *)
-end
-
-(** {2 Requests}
-
- Requests are sent by a client, e.g. a web browser or cURL.
- From the point of view of the server, they're inputs. *)
-
-module Request : sig
- type 'body t = private {
- meth: Meth.t; (** HTTP method for this request. *)
- host: string;
- (** Host header, mandatory. It can also be found in {!headers}. *)
- client_addr: Unix.sockaddr; (** Client address. Available since 0.14. *)
- headers: Headers.t; (** List of headers. *)
- http_version: int * int;
- (** HTTP version. This should be either [1, 0] or [1, 1]. *)
- path: string; (** Full path of the requested URL. *)
- path_components: string list;
- (** Components of the path of the requested URL. *)
- query: (string * string) list; (** Query part of the requested URL. *)
- body: 'body; (** Body of the request. *)
- start_time: float;
- (** Obtained via [get_time_s] in {!create}
- @since 0.11 *)
- }
- (** A request with method, path, host, headers, and a body, sent by a client.
-
- The body is polymorphic because the request goes through
- several transformations. First it has no body, as only the request
- and headers are read; then it has a stream body; then the body might be
- entirely read as a string via {!read_body_full}.
-
- @since 0.6 The field [query] was added and contains the query parameters in ["?foo=bar,x=y"]
- @since 0.6 The field [path_components] is the part of the path that precedes [query] and is split on ["/"].
- @since 0.11 the type is a private alias
- @since 0.11 the field [start_time] was added
- *)
-
- val pp : Format.formatter -> string t -> unit
- (** Pretty print the request and its body. The exact format of this printing
- is not specified. *)
-
- val pp_ : Format.formatter -> _ t -> unit
- (** Pretty print the request without its body. The exact format of this printing
- is not specified. *)
-
- val headers : _ t -> Headers.t
- (** List of headers of the request, including ["Host"]. *)
-
- val get_header : ?f:(string -> string) -> _ t -> string -> string option
- (** [get_header req h] looks up header [h] in [req]. It returns [None] if the
- header is not present. This is case insensitive and should be used
- rather than looking up [h] verbatim in [headers]. *)
-
- val get_header_int : _ t -> string -> int option
- (** Same as {!get_header} but also performs a string to integer conversion. *)
-
- val set_header : string -> string -> 'a t -> 'a t
- (** [set_header k v req] sets [k: v] in the request [req]'s headers. *)
-
- val remove_header : string -> 'a t -> 'a t
- (** Remove one instance of this header.
- @since NEXT_RELEASE *)
-
- val update_headers : (Headers.t -> Headers.t) -> 'a t -> 'a t
- (** Modify headers using the given function.
- @since 0.11 *)
-
- val set_body : 'a -> _ t -> 'a t
- (** [set_body b req] returns a new query whose body is [b].
- @since 0.11 *)
-
- val host : _ t -> string
- (** Host field of the request. It also appears in the headers. *)
-
- val client_addr : _ t -> Unix.sockaddr
- (** Client address of the request.
- @since 0.16 *)
-
- val meth : _ t -> Meth.t
- (** Method for the request. *)
-
- val path : _ t -> string
- (** Request path. *)
-
- val query : _ t -> (string * string) list
- (** Decode the query part of the {!path} field.
- @since 0.4 *)
-
- val body : 'b t -> 'b
- (** Request body, possibly empty. *)
-
- val start_time : _ t -> float
- (** time stamp (from {!Unix.gettimeofday}) after parsing the first line of the request
- @since 0.11 *)
-
- val limit_body_size : max_size:int -> byte_stream t -> byte_stream t
- (** Limit the body size to [max_size] bytes, or return
- a [413] error.
- @since 0.3
- *)
-
- val read_body_full :
- ?buf:Tiny_httpd_buf.t -> ?buf_size:int -> byte_stream t -> string t
- (** Read the whole body into a string. Potentially blocking.
-
- @param buf_size initial size of underlying buffer (since 0.11)
- @param buf the initial buffer (since 0.14)
- *)
-
- (**/**)
-
- (* for testing purpose, do not use. There is no guarantee of stability. *)
- module Internal_ : sig
- val parse_req_start :
- ?buf:buf ->
- client_addr:Unix.sockaddr ->
- get_time_s:(unit -> float) ->
- byte_stream ->
- unit t option
-
- val parse_body : ?buf:buf -> unit t -> byte_stream -> byte_stream t
- end
-
- (**/**)
-end
-
-(** {2 Response Codes} *)
-
-module Response_code : sig
- type t = int
- (** A standard HTTP code.
-
- https://tools.ietf.org/html/rfc7231#section-6 *)
-
- val ok : t
- (** The code [200] *)
-
- val not_found : t
- (** The code [404] *)
-
- val descr : t -> string
- (** A description of some of the error codes.
- NOTE: this is not complete (yet). *)
-
- val is_success : t -> bool
- (** [is_success code] is true iff [code] is in the [2xx] or [3xx] range.
- @since NEXT_RELEASE *)
-end
-
-(** {2 Responses}
-
- Responses are what a http server, such as {!Tiny_httpd}, send back to
- the client to answer a {!Request.t}*)
-
-module Response : sig
- type body =
- [ `String of string
- | `Stream of byte_stream
- | `Writer of Tiny_httpd_io.Writer.t
- | `Void ]
- (** Body of a response, either as a simple string,
- or a stream of bytes, or nothing (for server-sent events notably).
-
- - [`String str] replies with a body set to this string, and a known content-length.
- - [`Stream str] replies with a body made from this string, using chunked encoding.
- - [`Void] replies with no body.
- - [`Writer w] replies with a body created by the writer [w], using
- a chunked encoding.
- It is available since 0.14.
- *)
-
- type t = private {
- 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. *)
- body: body; (** Body of the response. Can be empty. *)
- }
- (** A response to send back to a client. *)
-
- val set_body : body -> t -> t
- (** Set the body of the response.
- @since 0.11 *)
-
- val set_header : string -> string -> t -> t
- (** Set a header.
- @since 0.11 *)
-
- val update_headers : (Headers.t -> Headers.t) -> t -> t
- (** Modify headers.
- @since 0.11 *)
-
- val remove_header : string -> t -> t
- (** Remove one instance of this header.
- @since NEXT_RELEASE *)
-
- val set_headers : Headers.t -> t -> t
- (** Set all headers.
- @since 0.11 *)
-
- val set_code : Response_code.t -> t -> t
- (** Set the response code.
- @since 0.11 *)
-
- val make_raw : ?headers:Headers.t -> code:Response_code.t -> string -> t
- (** Make a response from its raw components, with a string body.
- Use [""] to not send a body at all. *)
-
- val make_raw_stream :
- ?headers:Headers.t -> code:Response_code.t -> byte_stream -> t
- (** Same as {!make_raw} but with a stream body. The body will be sent with
- the chunked transfer-encoding. *)
-
- val make_void : ?headers:Headers.t -> code:int -> unit -> t
- (** Return a response without a body at all.
- @since 0.13 *)
-
- val make :
- ?headers:Headers.t ->
- ?code:int ->
- (body, Response_code.t * string) result ->
- t
- (** [make r] turns a result into a response.
-
- - [make (Ok body)] replies with [200] and the body.
- - [make (Error (code,msg))] replies with the given error code
- and message as body.
- *)
-
- val make_string :
- ?headers:Headers.t ->
- ?code:int ->
- (string, Response_code.t * string) result ->
- t
- (** Same as {!make} but with a string body. *)
-
- val make_writer :
- ?headers:Headers.t ->
- ?code:int ->
- (Tiny_httpd_io.Writer.t, Response_code.t * string) result ->
- t
- (** Same as {!make} but with a writer body. *)
-
- val make_stream :
- ?headers:Headers.t ->
- ?code:int ->
- (byte_stream, Response_code.t * string) result ->
- t
- (** Same as {!make} but with a stream body. *)
-
- val fail :
- ?headers:Headers.t -> code:int -> ('a, unit, string, t) format4 -> 'a
- (** Make the current request fail with the given code and message.
- Example: [fail ~code:404 "oh noes, %s not found" "waldo"].
- *)
-
- val fail_raise : code:int -> ('a, unit, string, 'b) format4 -> 'a
- (** Similar to {!fail} but raises an exception that exits the current handler.
- This should not be used outside of a (path) handler.
- Example: [fail_raise ~code:404 "oh noes, %s not found" "waldo"; never_executed()]
- *)
-
- val pp : Format.formatter -> t -> unit
- (** Pretty print the response. The exact format is not specified. *)
-end
-
-(** {2 Routing}
-
- Basic type-safe routing of handlers based on URL paths. This is optional,
- it is possible to only define the root handler with something like
- {{: https://github.com/anuragsoni/routes/} Routes}.
- @since 0.6 *)
-
-module Route : sig
- type ('a, 'b) comp
- (** An atomic component of a path *)
-
- type ('a, 'b) t
- (** A route, composed of path components *)
-
- val int : (int -> 'a, 'a) comp
- (** Matches an integer. *)
-
- val string : (string -> 'a, 'a) comp
- (** Matches a string not containing ['/'] and binds it as is. *)
-
- val string_urlencoded : (string -> 'a, 'a) comp
- (** Matches a URL-encoded string, and decodes it. *)
-
- val exact : string -> ('a, 'a) comp
- (** [exact "s"] matches ["s"] and nothing else. *)
-
- val return : ('a, 'a) t
- (** Matches the empty path. *)
-
- val rest_of_path : (string -> 'a, 'a) t
- (** Matches a string, even containing ['/']. This will match
- the entirety of the remaining route.
- @since 0.7 *)
-
- val rest_of_path_urlencoded : (string -> 'a, 'a) t
- (** Matches a string, even containing ['/'], an URL-decode it.
- This will match the entirety of the remaining route.
- @since 0.7 *)
-
- val ( @/ ) : ('a, 'b) comp -> ('b, 'c) t -> ('a, 'c) t
- (** [comp / route] matches ["foo/bar/…"] iff [comp] matches ["foo"],
- and [route] matches ["bar/…"]. *)
-
- val exact_path : string -> ('a, 'b) t -> ('a, 'b) t
- (** [exact_path "foo/bar/..." r] is equivalent to
- [exact "foo" @/ exact "bar" @/ ... @/ r]
- @since 0.11 **)
-
- val pp : Format.formatter -> _ t -> unit
- (** Print the route.
- @since 0.7 *)
-
- val to_string : _ t -> string
- (** Print the route.
- @since 0.7 *)
-end
-
-(** {2 Middlewares}
-
- A middleware can be inserted in a handler to modify or observe
- its behavior.
-
- @since 0.11
-*)
-
-module Middleware : sig
- type handler = byte_stream Request.t -> resp:(Response.t -> unit) -> unit
- (** Handlers are functions returning a response to a request.
- The response can be delayed, hence the use of a continuation
- as the [resp] parameter. *)
-
- type t = handler -> handler
- (** A middleware is a handler transformation.
-
- It takes the existing handler [h],
- and returns a new one which, given a query, modify it or log it
- before passing it to [h], or fail. It can also log or modify or drop
- the response. *)
-
- val nil : t
- (** Trivial middleware that does nothing. *)
-end
-
-(** {2 Main Server type} *)
-
-type t
-(** A HTTP server. See {!create} for more details. *)
-
-val create :
- ?masksigpipe:bool ->
- ?max_connections:int ->
- ?timeout:float ->
- ?buf_size:int ->
- ?get_time_s:(unit -> float) ->
- ?new_thread:((unit -> unit) -> unit) ->
- ?addr:string ->
- ?port:int ->
- ?sock:Unix.file_descr ->
- ?middlewares:([ `Encoding | `Stage of int ] * Middleware.t) list ->
- unit ->
- t
-(** Create a new webserver using UNIX abstractions.
-
- The server will not do anything until {!run} is called on it.
- Before starting the server, one can use {!add_path_handler} and
- {!set_top_handler} to specify how to handle incoming requests.
-
- @param masksigpipe if true, block the signal {!Sys.sigpipe} which otherwise
- tends to kill client threads when they try to write on broken sockets. Default: [true].
-
- @param buf_size size for buffers (since 0.11)
-
- @param new_thread a function used to spawn a new thread to handle a
- new client connection. By default it is {!Thread.create} but one
- could use a thread pool instead.
- See for example {{: https://github.com/c-cube/tiny-httpd-moonpool-bench/blob/0dcbbffb4fe34ea4ad79d46343ad0cebb69ca69f/examples/t1.ml#L31}
- this use of moonpool}.
-
- @param middlewares see {!add_middleware} for more details.
-
- @param max_connections maximum number of simultaneous connections.
- @param timeout connection is closed if the socket does not do read or
- write for the amount of second. Default: 0.0 which means no timeout.
- timeout is not recommended when using proxy.
- @param addr address (IPv4 or IPv6) to listen on. Default ["127.0.0.1"].
- @param port to listen on. Default [8080].
- @param sock an existing socket given to the server to listen on, e.g. by
- systemd on Linux (or launchd on macOS). If passed in, this socket will be
- used instead of the [addr] and [port]. If not passed in, those will be
- used. This parameter exists since 0.10.
-
- @param get_time_s obtain the current timestamp in seconds.
- This parameter exists since 0.11.
-*)
-
-(** A backend that provides IO operations, network operations, etc.
-
- This is used to decouple tiny_httpd from the scheduler/IO library used to
- actually open a TCP server and talk to clients. The classic way is
- based on {!Unix} and blocking IOs, but it's also possible to
- use an OCaml 5 library using effects and non blocking IOs. *)
-module type IO_BACKEND = sig
- val init_addr : unit -> string
- (** Initial TCP address *)
-
- val init_port : unit -> int
- (** Initial port *)
-
- val get_time_s : unit -> float
- (** Obtain the current timestamp in seconds. *)
-
- val tcp_server : unit -> Tiny_httpd_io.TCP_server.builder
- (** TCP server builder, to create servers that can listen
- on a port and handle clients. *)
-end
-
-val create_from :
- ?buf_size:int ->
- ?middlewares:([ `Encoding | `Stage of int ] * Middleware.t) list ->
- backend:(module IO_BACKEND) ->
- unit ->
- t
-(** Create a new webserver using provided backend.
-
- The server will not do anything until {!run} is called on it.
- Before starting the server, one can use {!add_path_handler} and
- {!set_top_handler} to specify how to handle incoming requests.
-
- @param buf_size size for buffers (since 0.11)
- @param middlewares see {!add_middleware} for more details.
-
- @since 0.14
-*)
-
-val addr : t -> string
-(** Address on which the server listens. *)
-
-val is_ipv6 : t -> bool
-(** [is_ipv6 server] returns [true] iff the address of the server is an IPv6 address.
- @since 0.3 *)
-
-val port : t -> int
-(** Port on which the server listens. Note that this might be different than
- the port initially given if the port was [0] (meaning that the OS picks a
- port for us). *)
-
-val active_connections : t -> int
-(** Number of currently active connections. *)
-
-val add_decode_request_cb :
- t ->
- (unit Request.t -> (unit Request.t * (byte_stream -> byte_stream)) option) ->
- unit
- [@@deprecated "use add_middleware"]
-(** Add a callback for every request.
- The callback can provide a stream transformer and a new request (with
- modified headers, typically).
- A possible use is to handle decompression by looking for a [Transfer-Encoding]
- header and returning a stream transformer that decompresses on the fly.
-
- @deprecated use {!add_middleware} instead
-*)
-
-val add_encode_response_cb :
- t -> (unit Request.t -> Response.t -> Response.t option) -> unit
- [@@deprecated "use add_middleware"]
-(** Add a callback for every request/response pair.
- Similarly to {!add_encode_response_cb} the callback can return a new
- response, for example to compress it.
- The callback is given the query with only its headers,
- as well as the current response.
-
- @deprecated use {!add_middleware} instead
-*)
-
-val add_middleware :
- stage:[ `Encoding | `Stage of int ] -> t -> Middleware.t -> unit
-(** Add a middleware to every request/response pair.
- @param stage specify when middleware applies.
- Encoding comes first (outermost layer), then stages in increasing order.
- @raise Invalid_argument if stage is [`Stage n] where [n < 1]
- @since 0.11
-*)
-
-(** {2 Request handlers} *)
-
-val set_top_handler : t -> (byte_stream Request.t -> Response.t) -> unit
-(** Setup a handler called by default.
-
- This handler is called with any request not accepted by any handler
- installed via {!add_path_handler}.
- If no top handler is installed, unhandled paths will return a [404] not found
-
- This used to take a [string Request.t] but it now takes a [byte_stream Request.t]
- since 0.14 . Use {!Request.read_body_full} to read the body into
- a string if needed.
-*)
-
-val add_route_handler :
- ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
- ?middlewares:Middleware.t list ->
- ?meth:Meth.t ->
- t ->
- ('a, string Request.t -> Response.t) Route.t ->
- 'a ->
- unit
-(** [add_route_handler server Route.(exact "path" @/ string @/ int @/ return) f]
- calls [f "foo" 42 request] when a [request] with path "path/foo/42/"
- is received.
-
- Note that the handlers are called in the reverse order of their addition,
- so the last registered handler can override previously registered ones.
-
- @param meth if provided, only accept requests with the given method.
- Typically one could react to [`GET] or [`PUT].
- @param accept should return [Ok()] if the given request (before its body
- is read) should be accepted, [Error (code,message)] if it's to be rejected (e.g. because
- its content is too big, or for some permission error).
- See the {!http_of_dir} program for an example of how to use [accept] to
- filter uploads that are too large before the upload even starts.
- The default always returns [Ok()], i.e. it accepts all requests.
-
- @since 0.6
-*)
-
-val add_route_handler_stream :
- ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
- ?middlewares:Middleware.t list ->
- ?meth:Meth.t ->
- t ->
- ('a, byte_stream Request.t -> Response.t) Route.t ->
- 'a ->
- unit
-(** Similar to {!add_route_handler}, but where the body of the request
- is a stream of bytes that has not been read yet.
- This is useful when one wants to stream the body directly into a parser,
- json decoder (such as [Jsonm]) or into a file.
- @since 0.6 *)
-
-(** {2 Server-sent events}
-
- {b EXPERIMENTAL}: this API is not stable yet. *)
-
-(** A server-side function to generate of Server-sent events.
-
- See {{: https://html.spec.whatwg.org/multipage/server-sent-events.html} the w3c page}
- and {{: https://jvns.ca/blog/2021/01/12/day-36--server-sent-events-are-cool--and-a-fun-bug/}
- this blog post}.
-
- @since 0.9
- *)
-module type SERVER_SENT_GENERATOR = sig
- val set_headers : Headers.t -> unit
- (** Set headers of the response.
- This is not mandatory but if used at all, it must be called before
- any call to {!send_event} (once events are sent the response is
- already sent too). *)
-
- val send_event :
- ?event:string -> ?id:string -> ?retry:string -> data:string -> unit -> unit
- (** Send an event from the server.
- If data is a multiline string, it will be sent on separate "data:" lines. *)
-
- val close : unit -> unit
- (** Close connection.
- @since 0.11 *)
-end
-
-type server_sent_generator = (module SERVER_SENT_GENERATOR)
-(** Server-sent event generator. This generates events that are forwarded to
- the client (e.g. the browser).
- @since 0.9 *)
-
-val add_route_server_sent_handler :
- ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
- t ->
- ('a, string Request.t -> server_sent_generator -> unit) Route.t ->
- 'a ->
- unit
-(** Add a handler on an endpoint, that serves server-sent events.
-
- The callback is given a generator that can be used to send events
- as it pleases. The connection is always closed by the client,
- and the accepted method is always [GET].
- This will set the header "content-type" to "text/event-stream" automatically
- and reply with a 200 immediately.
- See {!server_sent_generator} for more details.
-
- This handler stays on the original thread (it is synchronous).
-
- @since 0.9 *)
-
-(** {2 Upgrade handlers}
-
- These handlers upgrade the connection to another protocol.
- @since NEXT_RELEASE *)
-
-(** Handler that upgrades to another protocol.
- @since NEXT_RELEASE *)
-module type UPGRADE_HANDLER = sig
- type handshake_state
- (** Some specific state returned after handshake *)
-
- val name : string
- (** Name in the "upgrade" header *)
-
- val handshake : unit Request.t -> (Headers.t * handshake_state, string) result
- (** Perform the handshake and upgrade the connection. The returned
- code is [101] alongside these headers.
- In case the handshake fails, this only returns [Error log_msg].
- The connection is closed without further ado. *)
-
- val handle_connection :
- Unix.sockaddr ->
- handshake_state ->
- Tiny_httpd_io.Input.t ->
- Tiny_httpd_io.Output.t ->
- unit
- (** Take control of the connection and take it from ther.e *)
-end
-
-type upgrade_handler = (module UPGRADE_HANDLER)
-(** @since NEXT_RELEASE *)
-
-val add_upgrade_handler :
- ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
- t ->
- ('a, upgrade_handler) Route.t ->
- 'a ->
- unit
-
-(** {2 Run the server} *)
-
-val running : t -> bool
-(** Is the server running?
- @since 0.14 *)
-
-val stop : t -> unit
-(** Ask the server to stop. This might not have an immediate effect
- as {!run} might currently be waiting on IO. *)
-
-val run : ?after_init:(unit -> unit) -> t -> (unit, exn) result
-(** Run the main loop of the server, listening on a socket
- described at the server's creation time, using [new_thread] to
- start a thread for each new client.
-
- This returns [Ok ()] if the server exits gracefully, or [Error e] if
- it exits with an error.
-
- @param after_init is called after the server starts listening. since 0.13 .
-*)
-
-val run_exn : ?after_init:(unit -> unit) -> t -> unit
-(** [run_exn s] is like [run s] but re-raises an exception if the server exits
- with an error.
- @since 0.14 *)
diff --git a/src/bin/http_of_dir.ml b/src/bin/http_of_dir.ml
index 468f7e32..38f20820 100644
--- a/src/bin/http_of_dir.ml
+++ b/src/bin/http_of_dir.ml
@@ -1,6 +1,6 @@
module S = Tiny_httpd
-module U = Tiny_httpd_util
-module D = Tiny_httpd_dir
+module U = Tiny_httpd.Util
+module D = Tiny_httpd.Dir
module Pf = Printf
module Log = Tiny_httpd.Log
diff --git a/src/bin/vfs_pack.ml b/src/bin/vfs_pack.ml
index 8eacfeff..0b888ebc 100644
--- a/src/bin/vfs_pack.ml
+++ b/src/bin/vfs_pack.ml
@@ -33,12 +33,12 @@ let is_url s =
is_prefix "http://" s || is_prefix "https://" s
let emit oc (l : entry list) : unit =
- fpf oc "let embedded_fs = Tiny_httpd_dir.Embedded_fs.create ~mtime:%f ()\n"
+ fpf oc "let embedded_fs = Tiny_httpd.Dir.Embedded_fs.create ~mtime:%f ()\n"
now_;
let add_vfs ~mtime vfs_path content =
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\
\ %S\n"
mtime vfs_path content
@@ -99,7 +99,7 @@ let emit oc (l : entry list) : unit =
in
List.iter add_entry l;
- 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 =
diff --git a/src/camlzip/Tiny_httpd_camlzip.ml b/src/camlzip/Tiny_httpd_camlzip.ml
index 7d390211..4e8f5172 100644
--- a/src/camlzip/Tiny_httpd_camlzip.ml
+++ b/src/camlzip/Tiny_httpd_camlzip.ml
@@ -1,175 +1,59 @@
-module S = Tiny_httpd_server
-module BS = Tiny_httpd_stream
-module W = Tiny_httpd_io.Writer
-module Out = Tiny_httpd_io.Output
-module Log = Tiny_httpd.Log
+module W = IO.Writer
-let decode_deflate_stream_ ~buf_size (is : S.byte_stream) : S.byte_stream =
+(* TODO: just use iostream-camlzip? *)
+
+let decode_deflate_stream_ ~buf_size (ic : IO.Input.t) : IO.Input.t =
Log.debug (fun k -> k "wrap stream with deflate.decode");
- let zlib_str = Zlib.inflate_init false in
- let is_done = ref false in
- BS.make ~bs:(Bytes.create buf_size)
- ~close:(fun _ ->
- Zlib.inflate_end zlib_str;
- BS.close is)
- ~consume:(fun self len ->
- if len > self.len then
- S.Response.fail_raise ~code:400
- "inflate: error during decompression: invalid consume len %d (max %d)"
- len self.len;
- self.off <- self.off + len;
- self.len <- self.len - len)
- ~fill:(fun self ->
- (* refill [buf] if needed *)
- if self.len = 0 && not !is_done then (
- is.fill_buf ();
- (try
- let finished, used_in, used_out =
- Zlib.inflate zlib_str self.bs 0 (Bytes.length self.bs) is.bs is.off
- is.len Zlib.Z_SYNC_FLUSH
- in
- is.consume used_in;
- self.off <- 0;
- self.len <- used_out;
- if finished then is_done := true;
- Log.debug (fun k ->
- k "decode %d bytes as %d bytes from inflate (finished: %b)"
- used_in used_out finished)
- with Zlib.Error (e1, e2) ->
- S.Response.fail_raise ~code:400
- "inflate: error during decompression:\n%s %s" e1 e2);
- Log.debug (fun k ->
- k "inflate: refill %d bytes into internal buf" self.len)
- ))
- ()
+ Iostream_camlzip.decompress_in_buf ~buf_size ic
let encode_deflate_writer_ ~buf_size (w : W.t) : W.t =
Log.debug (fun k -> k "wrap writer with deflate.encode");
- let zlib_str = Zlib.deflate_init 4 false in
- let o_buf = Bytes.create buf_size in
- let o_off = ref 0 in
- let o_len = ref 0 in
-
- (* write output buffer to out *)
- let write_out (oc : Out.t) =
- if !o_len > 0 then (
- Out.output oc o_buf !o_off !o_len;
- o_off := 0;
- o_len := 0
- )
+ let { IO.Writer.write } = w in
+ let write' (oc : IO.Output.t) =
+ let oc' = Iostream_camlzip.compressed_out ~buf_size ~level:4 oc in
+ write (oc' :> IO.Output.t)
in
+ IO.Writer.make ~write:write' ()
- let flush_zlib ~flush (oc : Out.t) =
- let continue = ref true in
- while !continue do
- let finished, used_in, used_out =
- Zlib.deflate zlib_str Bytes.empty 0 0 o_buf 0 (Bytes.length o_buf) flush
- in
- assert (used_in = 0);
- o_len := !o_len + used_out;
- if finished then continue := false;
- write_out oc
- done
- in
-
- (* compress and consume input buffer *)
- let write_zlib ~flush (oc : Out.t) buf i len =
- let i = ref i in
- let len = ref len in
- while !len > 0 do
- let _finished, used_in, used_out =
- Zlib.deflate zlib_str buf !i !len o_buf 0 (Bytes.length o_buf) flush
- in
- i := !i + used_in;
- len := !len - used_in;
- o_len := !o_len + used_out;
- write_out oc
- done
- in
-
- let write (oc : Out.t) : unit =
- let output buf i len = write_zlib ~flush:Zlib.Z_NO_FLUSH oc buf i len in
-
- let bchar = Bytes.create 1 in
- let output_char c =
- Bytes.set bchar 0 c;
- output bchar 0 1
- in
-
- let flush () =
- flush_zlib oc ~flush:Zlib.Z_FINISH;
- assert (!o_len = 0);
- oc.flush ()
- in
- let close () =
- flush ();
- Zlib.deflate_end zlib_str;
- oc.close ()
- in
- (* new output channel that compresses on the fly *)
- let oc' = { Out.flush; close; output; output_char } in
- w.write oc';
- oc'.close ()
- in
-
- W.make ~write ()
-
-let split_on_char ?(f = fun x -> x) c s : string list =
- let rec loop acc i =
- match String.index_from s i c with
- | exception Not_found ->
- let acc =
- if i = String.length s then
- acc
- else
- f (String.sub s i (String.length s - i)) :: acc
- in
- List.rev acc
- | j ->
- let acc = f (String.sub s i (j - i)) :: acc in
- loop acc (j + 1)
- in
- loop [] 0
-
-let accept_deflate (req : _ S.Request.t) =
- match S.Request.get_header req "Accept-Encoding" with
- | Some s -> List.mem "deflate" @@ split_on_char ~f:String.trim ',' s
+let accept_deflate (req : _ Request.t) =
+ match Request.get_header req "Accept-Encoding" with
+ | Some s ->
+ List.mem "deflate" @@ List.rev_map String.trim @@ String.split_on_char ',' s
| None -> false
let has_deflate s =
try Scanf.sscanf s "deflate, %s" (fun _ -> true) with _ -> false
(* decompress [req]'s body if needed *)
-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
+let decompress_req_stream_ ~buf_size (req : IO.Input.t Request.t) : _ Request.t
+ =
+ match Request.get_header ~f:String.trim req "Transfer-Encoding" with
(* TODO
| Some "gzip" ->
let req' = S.Request.set_header req "Transfer-Encoding" "chunked" in
Some (req', decode_gzip_stream_)
*)
| Some "deflate" ->
- let body' = S.Request.body req |> decode_deflate_stream_ ~buf_size in
- req
- |> S.Request.remove_header "Transfer-Encoding"
- |> S.Request.set_body body'
+ let body' = Request.body req |> decode_deflate_stream_ ~buf_size in
+ req |> Request.remove_header "Transfer-Encoding" |> Request.set_body body'
| Some s when has_deflate s ->
(match Scanf.sscanf s "deflate, %s" (fun s -> s) with
| tr' ->
- let body' = S.Request.body req |> decode_deflate_stream_ ~buf_size in
+ let body' = Request.body req |> decode_deflate_stream_ ~buf_size in
req
- |> S.Request.set_header "Transfer-Encoding" tr'
- |> S.Request.set_body body'
+ |> Request.set_header "Transfer-Encoding" tr'
+ |> Request.set_body body'
| exception _ -> req)
| _ -> req
-let compress_resp_stream_ ~compress_above ~buf_size (req : _ S.Request.t)
- (resp : S.Response.t) : S.Response.t =
+let compress_resp_stream_ ~compress_above ~buf_size (req : _ Request.t)
+ (resp : Response.t) : Response.t =
(* headers for compressed stream *)
let update_headers h =
h
- |> S.Headers.remove "Content-Length"
- |> S.Headers.set "Content-Encoding" "deflate"
+ |> Headers.remove "Content-Length"
+ |> Headers.set "Content-Encoding" "deflate"
in
if accept_deflate req then (
@@ -181,25 +65,25 @@ let compress_resp_stream_ ~compress_above ~buf_size (req : _ S.Request.t)
(String.length s) compress_above);
let body = encode_deflate_writer_ ~buf_size @@ W.of_string s in
resp
- |> S.Response.update_headers update_headers
- |> S.Response.set_body (`Writer body)
- | `Stream str ->
+ |> Response.update_headers update_headers
+ |> Response.set_body (`Writer body)
+ | `Stream ic ->
Log.debug (fun k -> k "encode stream response with deflate");
- let w = BS.to_writer str in
+ let w = IO.Writer.of_input ic in
resp
- |> S.Response.update_headers update_headers
- |> S.Response.set_body (`Writer (encode_deflate_writer_ ~buf_size w))
+ |> Response.update_headers update_headers
+ |> Response.set_body (`Writer (encode_deflate_writer_ ~buf_size w))
| `Writer w ->
Log.debug (fun k -> k "encode writer response with deflate");
resp
- |> S.Response.update_headers update_headers
- |> S.Response.set_body (`Writer (encode_deflate_writer_ ~buf_size w))
+ |> Response.update_headers update_headers
+ |> Response.set_body (`Writer (encode_deflate_writer_ ~buf_size w))
| `String _ | `Void -> resp
) else
resp
let middleware ?(compress_above = 16 * 1024) ?(buf_size = 16 * 1_024) () :
- S.Middleware.t =
+ Server.Middleware.t =
let buf_size = max buf_size 1_024 in
fun h req ~resp ->
let req = decompress_req_stream_ ~buf_size req in
@@ -209,4 +93,4 @@ let middleware ?(compress_above = 16 * 1024) ?(buf_size = 16 * 1_024) () :
let setup ?compress_above ?buf_size server =
let m = middleware ?compress_above ?buf_size () in
Log.info (fun k -> k "setup gzip middleware");
- S.add_middleware ~stage:`Encoding server m
+ Server.add_middleware ~stage:`Encoding server m
diff --git a/src/camlzip/Tiny_httpd_camlzip.mli b/src/camlzip/Tiny_httpd_camlzip.mli
index 2fb7f570..f098e6da 100644
--- a/src/camlzip/Tiny_httpd_camlzip.mli
+++ b/src/camlzip/Tiny_httpd_camlzip.mli
@@ -7,7 +7,7 @@
*)
val middleware :
- ?compress_above:int -> ?buf_size:int -> unit -> Tiny_httpd_server.Middleware.t
+ ?compress_above:int -> ?buf_size:int -> unit -> Server.Middleware.t
(** Middleware responsible for deflate compression/decompression.
@param compress_above threshold, in bytes, above which a response body
that has a known content-length is compressed. Stream bodies
@@ -15,7 +15,7 @@ val middleware :
@param buf_size size of the underlying buffer for compression/decompression
@since 0.11 *)
-val setup : ?compress_above:int -> ?buf_size:int -> Tiny_httpd_server.t -> unit
+val setup : ?compress_above:int -> ?buf_size:int -> Server.t -> unit
(** Install middleware for tiny_httpd to be able to encode/decode
compressed streams
@param compress_above threshold above with string responses are compressed
diff --git a/src/camlzip/dune b/src/camlzip/dune
index 6ffc109f..d7192304 100644
--- a/src/camlzip/dune
+++ b/src/camlzip/dune
@@ -2,5 +2,5 @@
(name tiny_httpd_camlzip)
(public_name tiny_httpd_camlzip)
(synopsis "A wrapper around camlzip to bring compression to Tiny_httpd")
- (flags :standard -safe-string -warn-error -a+8)
- (libraries tiny_httpd camlzip))
+ (flags :standard -open Tiny_httpd_core -safe-string -warn-error -a+8)
+ (libraries tiny_httpd.core iostream-camlzip camlzip))
diff --git a/src/core/IO.ml b/src/core/IO.ml
new file mode 100644
index 00000000..249da955
--- /dev/null
+++ b/src/core/IO.ml
@@ -0,0 +1,478 @@
+(** IO abstraction.
+
+ We abstract IO so we can support classic unix blocking IOs
+ with threads, and modern async IO with Eio.
+
+ {b NOTE}: experimental.
+
+ @since 0.14
+*)
+
+open Common_
+module Buf = Buf
+module Slice = Iostream.Slice
+
+(** Output channel (byte sink) *)
+module Output = struct
+ include Iostream.Out_buf
+
+ class of_unix_fd ?(close_noerr = false) ~closed ~(buf : Slice.t)
+ (fd : Unix.file_descr) : t =
+ object
+ inherit t_from_output ~bytes:buf.bytes ()
+
+ method private output_underlying bs i len0 =
+ let i = ref i in
+ let len = ref len0 in
+ while !len > 0 do
+ match Unix.write fd bs !i !len with
+ | 0 -> failwith "write failed"
+ | n ->
+ i := !i + n;
+ len := !len - n
+ | exception
+ Unix.Unix_error
+ ( ( Unix.EBADF | Unix.ENOTCONN | Unix.ESHUTDOWN
+ | Unix.ECONNRESET | Unix.EPIPE ),
+ _,
+ _ ) ->
+ failwith "write failed"
+ | exception
+ Unix.Unix_error
+ ((Unix.EWOULDBLOCK | Unix.EAGAIN | Unix.EINTR), _, _) ->
+ ignore (Unix.select [] [ fd ] [] 1.)
+ done
+
+ method private close_underlying () =
+ if not !closed then (
+ closed := true;
+ if close_noerr then (
+ try Unix.close fd with _ -> ()
+ ) else
+ Unix.close fd
+ )
+ end
+
+ let output_buf (self : t) (buf : Buf.t) : unit =
+ let b = Buf.bytes_slice buf in
+ output self b 0 (Buf.size buf)
+
+ (** [chunk_encoding oc] makes a new channel that outputs its content into [oc]
+ in chunk encoding form.
+ @param close_rec if true, closing the result will also close [oc]
+ @param buf a buffer used to accumulate data into chunks.
+ Chunks are emitted when [buf]'s size gets over a certain threshold,
+ or when [flush] is called.
+ *)
+ let chunk_encoding ?(buf = Buf.create ()) ~close_rec (oc : #t) : t =
+ (* write content of [buf] as a chunk if it's big enough.
+ If [force=true] then write content of [buf] if it's simply non empty. *)
+ let write_buf ~force () =
+ let n = Buf.size buf in
+ if (force && n > 0) || n >= 4_096 then (
+ output_string oc (Printf.sprintf "%x\r\n" n);
+ output oc (Buf.bytes_slice buf) 0 n;
+ output_string oc "\r\n";
+ Buf.clear buf
+ )
+ in
+
+ object
+ method flush () =
+ write_buf ~force:true ();
+ flush oc
+
+ method close () =
+ write_buf ~force:true ();
+ (* write an empty chunk to close the stream *)
+ output_string oc "0\r\n";
+ (* write another crlf after the stream (see #56) *)
+ output_string oc "\r\n";
+ flush oc;
+ if close_rec then close oc
+
+ method output b i n =
+ Buf.add_bytes buf b i n;
+ write_buf ~force:false ()
+
+ method output_char c =
+ Buf.add_char buf c;
+ write_buf ~force:false ()
+ end
+end
+
+(** Input channel (byte source) *)
+module Input = struct
+ include Iostream.In_buf
+
+ let of_unix_fd ?(close_noerr = false) ~closed ~(buf : Slice.t)
+ (fd : Unix.file_descr) : t =
+ let eof = ref false in
+ object
+ inherit Iostream.In_buf.t_from_refill ~bytes:buf.bytes ()
+
+ method private refill (slice : Slice.t) =
+ if not !eof then (
+ slice.off <- 0;
+ let continue = ref true in
+ while !continue do
+ match Unix.read fd slice.bytes 0 (Bytes.length slice.bytes) with
+ | n ->
+ slice.len <- n;
+ continue := false
+ | exception
+ Unix.Unix_error
+ ( ( Unix.EBADF | Unix.ENOTCONN | Unix.ESHUTDOWN
+ | Unix.ECONNRESET | Unix.EPIPE ),
+ _,
+ _ ) ->
+ eof := true;
+ continue := false
+ | exception
+ Unix.Unix_error
+ ((Unix.EWOULDBLOCK | Unix.EAGAIN | Unix.EINTR), _, _) ->
+ ignore (Unix.select [ fd ] [] [] 1.)
+ done;
+ (* Printf.eprintf "read returned %d B\n%!" !n; *)
+ if slice.len = 0 then eof := true
+ )
+
+ method close () =
+ if not !closed then (
+ closed := true;
+ eof := true;
+ if close_noerr then (
+ try Unix.close fd with _ -> ()
+ ) else
+ Unix.close fd
+ )
+ end
+
+ let of_slice (slice : Slice.t) : t =
+ object
+ inherit Iostream.In_buf.t_from_refill ~bytes:slice.bytes ()
+
+ method private refill (slice : Slice.t) =
+ slice.off <- 0;
+ slice.len <- 0
+
+ method close () = ()
+ end
+
+ (** Read into the given slice.
+ @return the number of bytes read, [0] means end of input. *)
+ let[@inline] input (self : t) buf i len = self#input buf i len
+
+ (** Close the channel. *)
+ let[@inline] close self : unit = self#close ()
+
+ (** Read exactly [len] bytes.
+ @raise End_of_file if the input did not contain enough data. *)
+ let really_input (self : t) buf i len : unit =
+ let i = ref i in
+ let len = ref len in
+ while !len > 0 do
+ let n = input self buf !i !len in
+ if n = 0 then raise End_of_file;
+ i := !i + n;
+ len := !len - n
+ done
+
+ let append (i1 : #t) (i2 : #t) : t =
+ let use_i1 = ref true in
+ let rec input_rec (slice : Slice.t) =
+ if !use_i1 then (
+ slice.len <- input i1 slice.bytes 0 (Bytes.length slice.bytes);
+ if slice.len = 0 then (
+ use_i1 := false;
+ input_rec slice
+ )
+ ) else
+ slice.len <- input i1 slice.bytes 0 (Bytes.length slice.bytes)
+ in
+
+ object
+ inherit Iostream.In_buf.t_from_refill ()
+
+ method private refill (slice : Slice.t) =
+ slice.off <- 0;
+ input_rec slice
+
+ method close () =
+ close i1;
+ close i2
+ end
+
+ let iter_slice (f : Slice.t -> unit) (self : #t) : unit =
+ let continue = ref true in
+ while !continue do
+ let slice = self#fill_buf () in
+ if slice.len = 0 then (
+ continue := false;
+ close self
+ ) else (
+ f slice;
+ Slice.consume slice slice.len
+ )
+ done
+
+ let iter f self =
+ iter_slice (fun (slice : Slice.t) -> f slice.bytes slice.off slice.len) self
+
+ let to_chan oc (self : #t) =
+ iter_slice
+ (fun (slice : Slice.t) ->
+ Stdlib.output oc slice.bytes slice.off slice.len)
+ self
+
+ let to_chan' (oc : #Iostream.Out.t) (self : #t) : unit =
+ iter_slice
+ (fun (slice : Slice.t) ->
+ Iostream.Out.output oc slice.bytes slice.off slice.len)
+ self
+
+ let read_all_using ~buf (self : #t) : string =
+ Buf.clear buf;
+ let continue = ref true in
+ while !continue do
+ let slice = fill_buf self in
+ if slice.len = 0 then
+ continue := false
+ else (
+ assert (slice.len > 0);
+ Buf.add_bytes buf slice.bytes slice.off slice.len;
+ Slice.consume slice slice.len
+ )
+ done;
+ Buf.contents_and_clear buf
+
+ (** Read [n] bytes from the input into [bytes]. *)
+ let read_exactly_ ~too_short (self : #t) (bytes : bytes) (n : int) : unit =
+ assert (Bytes.length bytes >= n);
+ let offset = ref 0 in
+ while !offset < n do
+ let slice = self#fill_buf () in
+ let n_read = min slice.len (n - !offset) in
+ Bytes.blit slice.bytes slice.off bytes !offset n_read;
+ offset := !offset + n_read;
+ Slice.consume slice n_read;
+ if n_read = 0 then too_short ()
+ done
+
+ (** read a line into the buffer, after clearing it. *)
+ let read_line_into (self : t) ~buf : unit =
+ Buf.clear buf;
+ let continue = ref true in
+ while !continue do
+ let slice = self#fill_buf () in
+ if slice.len = 0 then (
+ continue := false;
+ if Buf.size buf = 0 then raise End_of_file
+ );
+ let j = ref slice.off in
+ while !j < slice.off + slice.len && Bytes.get slice.bytes !j <> '\n' do
+ incr j
+ done;
+ if !j - slice.off < slice.len then (
+ assert (Bytes.get slice.bytes !j = '\n');
+ (* line without '\n' *)
+ Buf.add_bytes buf slice.bytes slice.off (!j - slice.off);
+ (* consume line + '\n' *)
+ Slice.consume slice (!j - slice.off + 1);
+ continue := false
+ ) else (
+ Buf.add_bytes buf slice.bytes slice.off slice.len;
+ Slice.consume slice slice.len
+ )
+ done
+
+ let read_line_using ~buf (self : #t) : string =
+ read_line_into self ~buf;
+ Buf.contents_and_clear buf
+
+ let read_line_using_opt ~buf (self : #t) : string option =
+ match read_line_into self ~buf with
+ | () -> Some (Buf.contents_and_clear buf)
+ | exception End_of_file -> None
+
+ (* helper for making a new input stream that either contains at most [size]
+ bytes, or contains exactly [size] bytes. *)
+ let reading_exactly_ ~skip_on_close ~close_rec ~size ~bytes (arg : t) : t =
+ let remaining_size = ref size in
+
+ object
+ inherit t_from_refill ~bytes ()
+
+ method close () =
+ if !remaining_size > 0 && skip_on_close then skip arg !remaining_size;
+ if close_rec then close arg
+
+ method private refill (slice : Slice.t) =
+ slice.off <- 0;
+ slice.len <- 0;
+ if !remaining_size > 0 then (
+ let sub = fill_buf arg in
+ let n =
+ min !remaining_size (min sub.len (Bytes.length slice.bytes))
+ in
+ Bytes.blit sub.bytes sub.off slice.bytes 0 n;
+ Slice.consume sub n;
+ remaining_size := !remaining_size - n;
+ slice.len <- n
+ )
+ end
+
+ (** new stream with maximum size [max_size].
+ @param close_rec if true, closing this will also close the input stream *)
+ let limit_size_to ~close_rec ~max_size ~bytes (arg : t) : t =
+ reading_exactly_ ~size:max_size ~skip_on_close:false ~bytes ~close_rec arg
+
+ (** New stream that consumes exactly [size] bytes from the input.
+ If fewer bytes are read before [close] is called, we read and discard
+ the remaining quota of bytes before [close] returns.
+ @param close_rec if true, closing this will also close the input stream *)
+ let reading_exactly ~close_rec ~size ~bytes (arg : t) : t =
+ reading_exactly_ ~size ~close_rec ~skip_on_close:true ~bytes arg
+
+ let read_chunked ~(bytes : bytes) ~fail (ic : #t) : t =
+ let first = ref true in
+
+ (* small buffer to read the chunk sizes *)
+ let line_buf = Buf.create ~size:32 () in
+ let read_next_chunk_len () : int =
+ if !first then
+ first := false
+ else (
+ let line = read_line_using ~buf:line_buf ic in
+ if String.trim line <> "" then
+ raise (fail "expected crlf between chunks")
+ );
+ let line = read_line_using ~buf:line_buf ic in
+ (* parse chunk length, ignore extensions *)
+ let chunk_size =
+ if String.trim line = "" then
+ 0
+ else (
+ try
+ let off = ref 0 in
+ let n = Parse_.pos_hex line off in
+ n
+ with _ ->
+ raise (fail (spf "cannot read chunk size from line %S" line))
+ )
+ in
+ chunk_size
+ in
+ let eof = ref false in
+ let chunk_size = ref 0 in
+
+ object
+ inherit t_from_refill ~bytes ()
+
+ method private refill (slice : Slice.t) : unit =
+ if !chunk_size = 0 && not !eof then (
+ chunk_size := read_next_chunk_len ();
+ if !chunk_size = 0 then eof := true (* stream is finished *)
+ );
+ slice.off <- 0;
+ slice.len <- 0;
+ if !chunk_size > 0 then (
+ (* read the whole chunk, or [Bytes.length bytes] of it *)
+ let to_read = min !chunk_size (Bytes.length slice.bytes) in
+ read_exactly_
+ ~too_short:(fun () -> raise (fail "chunk is too short"))
+ ic slice.bytes to_read;
+ slice.len <- to_read;
+ chunk_size := !chunk_size - to_read
+ )
+
+ method close () = eof := true (* do not close underlying stream *)
+ end
+
+ (** Output a stream using chunked encoding *)
+ let output_chunked' ?buf (oc : #Iostream.Out_buf.t) (self : #t) : unit =
+ let oc' = Output.chunk_encoding ?buf oc ~close_rec:false in
+ match to_chan' oc' self with
+ | () -> Output.close oc'
+ | exception e ->
+ let bt = Printexc.get_raw_backtrace () in
+ Output.close oc';
+ Printexc.raise_with_backtrace e bt
+
+ (** print a stream as a series of chunks *)
+ let output_chunked ?buf (oc : out_channel) (self : #t) : unit =
+ output_chunked' ?buf (Output.of_out_channel oc) self
+end
+
+(** A writer abstraction. *)
+module Writer = struct
+ type t = { write: Output.t -> unit } [@@unboxed]
+ (** Writer.
+
+ A writer is a push-based stream of bytes.
+ Give it an output channel and it will write the bytes in it.
+
+ This is useful for responses: an http endpoint can return a writer
+ as its response's body; the writer is given access to the connection
+ to the client and can write into it as if it were a regular
+ [out_channel], including controlling calls to [flush].
+ Tiny_httpd will convert these writes into valid HTTP chunks.
+ @since 0.14
+ *)
+
+ let[@inline] make ~write () : t = { write }
+
+ (** Write into the channel. *)
+ let[@inline] write (oc : #Output.t) (self : t) : unit =
+ self.write (oc :> Output.t)
+
+ (** Empty writer, will output 0 bytes. *)
+ let empty : t = { write = ignore }
+
+ (** A writer that just emits the bytes from the given string. *)
+ let[@inline] of_string (str : string) : t =
+ let write oc = Iostream.Out.output_string oc str in
+ { write }
+
+ let[@inline] of_input (ic : #Input.t) : t =
+ { write = (fun oc -> Input.to_chan' oc ic) }
+end
+
+(** A TCP server abstraction. *)
+module TCP_server = struct
+ type conn_handler = {
+ handle: client_addr:Unix.sockaddr -> Input.t -> Output.t -> unit;
+ (** Handle client connection *)
+ }
+
+ type t = {
+ endpoint: unit -> string * int;
+ (** Endpoint we listen on. This can only be called from within [serve]. *)
+ active_connections: unit -> int;
+ (** Number of connections currently active *)
+ running: unit -> bool; (** Is the server currently running? *)
+ stop: unit -> unit;
+ (** Ask the server to stop. This might not take effect immediately,
+ and is idempotent. After this [server.running()] must return [false]. *)
+ }
+ (** A running TCP server.
+
+ This contains some functions that provide information about the running
+ server, including whether it's active (as opposed to stopped), a function
+ to stop it, and statistics about the number of connections. *)
+
+ type builder = {
+ serve: after_init:(t -> unit) -> handle:conn_handler -> unit -> unit;
+ (** Blocking call to listen for incoming connections and handle them.
+ Uses the connection handler [handle] to handle individual client
+ connections in individual threads/fibers/tasks.
+ @param after_init is called once with the server after the server
+ has started. *)
+ }
+ (** A TCP server builder implementation.
+
+ Calling [builder.serve ~after_init ~handle ()] starts a new TCP server on
+ an unspecified endpoint
+ (most likely coming from the function returning this builder)
+ and returns the running server. *)
+end
diff --git a/src/Tiny_httpd_buf.ml b/src/core/buf.ml
similarity index 96%
rename from src/Tiny_httpd_buf.ml
rename to src/core/buf.ml
index fcc89933..5824946f 100644
--- a/src/Tiny_httpd_buf.ml
+++ b/src/core/buf.ml
@@ -4,6 +4,7 @@ let create ?(size = 4_096) () : t =
let bytes = Bytes.make size ' ' in
{ bytes; i = 0; original = bytes }
+let of_bytes bytes : t = { bytes; i = 0; original = bytes }
let[@inline] size self = self.i
let[@inline] bytes_slice self = self.bytes
diff --git a/src/Tiny_httpd_buf.mli b/src/core/buf.mli
similarity index 97%
rename from src/Tiny_httpd_buf.mli
rename to src/core/buf.mli
index e5ca90c1..dbbbb7ca 100644
--- a/src/Tiny_httpd_buf.mli
+++ b/src/core/buf.mli
@@ -11,6 +11,7 @@ type t
val size : t -> int
val clear : t -> unit
val create : ?size:int -> unit -> t
+val of_bytes : bytes -> t
val contents : t -> string
val clear_and_zero : t -> unit
diff --git a/src/core/common_.ml b/src/core/common_.ml
new file mode 100644
index 00000000..1058feea
--- /dev/null
+++ b/src/core/common_.ml
@@ -0,0 +1,10 @@
+exception Bad_req of int * string
+
+let spf = Printf.sprintf
+let bad_reqf c fmt = Printf.ksprintf (fun s -> raise (Bad_req (c, s))) fmt
+
+type 'a resp_result = ('a, int * string) result
+
+let unwrap_resp_result = function
+ | Ok x -> x
+ | Error (c, s) -> raise (Bad_req (c, s))
diff --git a/src/core/dune b/src/core/dune
new file mode 100644
index 00000000..a04707ef
--- /dev/null
+++ b/src/core/dune
@@ -0,0 +1,18 @@
+
+(library
+ (name tiny_httpd_core)
+ (public_name tiny_httpd.core)
+ (private_modules parse_ common_)
+ (libraries threads seq hmap iostream
+ (select log.ml from
+ (logs -> log.logs.ml)
+ (-> log.default.ml))))
+
+(rule
+ (targets Atomic_.ml)
+ (deps
+ (:bin ./gen/mkshims.exe))
+ (action
+ (with-stdout-to
+ %{targets}
+ (run %{bin}))))
diff --git a/src/core/gen/dune b/src/core/gen/dune
new file mode 100644
index 00000000..cf54b00e
--- /dev/null
+++ b/src/core/gen/dune
@@ -0,0 +1,2 @@
+(executables
+ (names mkshims))
diff --git a/src/gen/mkshims.ml b/src/core/gen/mkshims.ml
similarity index 100%
rename from src/gen/mkshims.ml
rename to src/core/gen/mkshims.ml
diff --git a/src/core/headers.ml b/src/core/headers.ml
new file mode 100644
index 00000000..a06a6439
--- /dev/null
+++ b/src/core/headers.ml
@@ -0,0 +1,70 @@
+open Common_
+
+type t = (string * string) list
+
+let empty = []
+
+let contains name headers =
+ let name' = String.lowercase_ascii name in
+ List.exists (fun (n, _) -> name' = n) headers
+
+let get_exn ?(f = fun x -> x) x h =
+ let x' = String.lowercase_ascii x in
+ List.assoc x' h |> f
+
+let get ?(f = fun x -> x) x h =
+ try Some (get_exn ~f x h) with Not_found -> None
+
+let remove x h =
+ let x' = String.lowercase_ascii x in
+ List.filter (fun (k, _) -> k <> x') h
+
+let set x y h =
+ let x' = String.lowercase_ascii x in
+ (x', y) :: List.filter (fun (k, _) -> k <> x') h
+
+let pp out l =
+ let pp_pair out (k, v) = Format.fprintf out "@[%s: %s@]" k v in
+ Format.fprintf out "@[%a@]" (Format.pp_print_list pp_pair) l
+
+(* token = 1*tchar
+ tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_"
+ / "`" / "|" / "~" / DIGIT / ALPHA ; any VCHAR, except delimiters
+ Reference: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 *)
+let is_tchar = function
+ | '0' .. '9'
+ | 'a' .. 'z'
+ | 'A' .. 'Z'
+ | '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`'
+ | '|' | '~' ->
+ true
+ | _ -> false
+
+let for_all pred s =
+ try
+ String.iter (fun c -> if not (pred c) then raise Exit) s;
+ true
+ with Exit -> false
+
+let parse_ ~(buf : Buf.t) (bs : IO.Input.t) : t =
+ let rec loop acc =
+ match IO.Input.read_line_using_opt ~buf bs with
+ | None -> raise End_of_file
+ | Some "\r" -> acc
+ | Some line ->
+ Log.debug (fun k -> k "parsed header line %S" line);
+ let k, v =
+ try
+ let i = String.index line ':' in
+ let k = String.sub line 0 i in
+ if not (for_all is_tchar k) then
+ invalid_arg (Printf.sprintf "Invalid header key: %S" k);
+ let v =
+ String.sub line (i + 1) (String.length line - i - 1) |> String.trim
+ in
+ k, v
+ with _ -> bad_reqf 400 "invalid header line: %S" line
+ in
+ loop ((String.lowercase_ascii k, v) :: acc)
+ in
+ loop []
diff --git a/src/core/headers.mli b/src/core/headers.mli
new file mode 100644
index 00000000..b46b5d54
--- /dev/null
+++ b/src/core/headers.mli
@@ -0,0 +1,35 @@
+(** Headers
+
+ Headers are metadata associated with a request or response. *)
+
+type t = (string * string) list
+(** The header files of a request or response.
+
+ Neither the key nor the value can contain ['\r'] or ['\n'].
+ See https://tools.ietf.org/html/rfc7230#section-3.2 *)
+
+val empty : t
+(** Empty list of headers.
+ @since 0.5 *)
+
+val get : ?f:(string -> string) -> string -> t -> string option
+(** [get k headers] looks for the header field with key [k].
+ @param f if provided, will transform the value before it is returned. *)
+
+val get_exn : ?f:(string -> string) -> string -> t -> string
+(** @raise Not_found *)
+
+val set : string -> string -> t -> t
+(** [set k v headers] sets the key [k] to value [v].
+ It erases any previous entry for [k] *)
+
+val remove : string -> t -> t
+(** Remove the key from the headers, if present. *)
+
+val contains : string -> t -> bool
+(** Is there a header with the given key? *)
+
+val pp : Format.formatter -> t -> unit
+(** Pretty print the headers. *)
+
+val parse_ : buf:Buf.t -> IO.Input.t -> t
diff --git a/src/Tiny_httpd_log.default.ml b/src/core/log.default.ml
similarity index 100%
rename from src/Tiny_httpd_log.default.ml
rename to src/core/log.default.ml
diff --git a/src/Tiny_httpd_log.logs.ml b/src/core/log.logs.ml
similarity index 100%
rename from src/Tiny_httpd_log.logs.ml
rename to src/core/log.logs.ml
diff --git a/src/Tiny_httpd_log.mli b/src/core/log.mli
similarity index 100%
rename from src/Tiny_httpd_log.mli
rename to src/core/log.mli
diff --git a/src/core/meth.ml b/src/core/meth.ml
new file mode 100644
index 00000000..94e6bb3a
--- /dev/null
+++ b/src/core/meth.ml
@@ -0,0 +1,22 @@
+open Common_
+
+type t = [ `GET | `PUT | `POST | `HEAD | `DELETE | `OPTIONS ]
+
+let to_string = function
+ | `GET -> "GET"
+ | `PUT -> "PUT"
+ | `HEAD -> "HEAD"
+ | `POST -> "POST"
+ | `DELETE -> "DELETE"
+ | `OPTIONS -> "OPTIONS"
+
+let pp out s = Format.pp_print_string out (to_string s)
+
+let of_string = function
+ | "GET" -> `GET
+ | "PUT" -> `PUT
+ | "POST" -> `POST
+ | "HEAD" -> `HEAD
+ | "DELETE" -> `DELETE
+ | "OPTIONS" -> `OPTIONS
+ | s -> bad_reqf 400 "unknown method %S" s
diff --git a/src/core/meth.mli b/src/core/meth.mli
new file mode 100644
index 00000000..76b2c942
--- /dev/null
+++ b/src/core/meth.mli
@@ -0,0 +1,11 @@
+(** HTTP Methods *)
+
+type t = [ `GET | `PUT | `POST | `HEAD | `DELETE | `OPTIONS ]
+(** A HTTP method.
+ For now we only handle a subset of these.
+
+ See https://tools.ietf.org/html/rfc7231#section-4 *)
+
+val pp : Format.formatter -> t -> unit
+val to_string : t -> string
+val of_string : string -> t
diff --git a/src/Tiny_httpd_parse_.ml b/src/core/parse_.ml
similarity index 100%
rename from src/Tiny_httpd_parse_.ml
rename to src/core/parse_.ml
diff --git a/src/Tiny_httpd_pool.ml b/src/core/pool.ml
similarity index 97%
rename from src/Tiny_httpd_pool.ml
rename to src/core/pool.ml
index 1a441944..fc9a0461 100644
--- a/src/Tiny_httpd_pool.ml
+++ b/src/core/pool.ml
@@ -1,4 +1,4 @@
-module A = Tiny_httpd_atomic_
+module A = Atomic_
type 'a list_ = Nil | Cons of int * 'a * 'a list_
diff --git a/src/Tiny_httpd_pool.mli b/src/core/pool.mli
similarity index 100%
rename from src/Tiny_httpd_pool.mli
rename to src/core/pool.mli
diff --git a/src/core/request.ml b/src/core/request.ml
new file mode 100644
index 00000000..1a3275df
--- /dev/null
+++ b/src/core/request.ml
@@ -0,0 +1,231 @@
+open Common_
+
+type 'body t = {
+ meth: Meth.t;
+ host: string;
+ client_addr: Unix.sockaddr;
+ headers: Headers.t;
+ mutable meta: Hmap.t;
+ http_version: int * int;
+ path: string;
+ path_components: string list;
+ query: (string * string) list;
+ body: 'body;
+ start_time: float;
+}
+
+let headers self = self.headers
+let host self = self.host
+let client_addr self = self.client_addr
+let meth self = self.meth
+let path self = self.path
+let body self = self.body
+let start_time self = self.start_time
+let query self = self.query
+let get_header ?f self h = Headers.get ?f h self.headers
+let remove_header k self = { self with headers = Headers.remove k self.headers }
+let add_meta self k v = self.meta <- Hmap.add k v self.meta
+let get_meta self k = Hmap.find k self.meta
+let get_meta_exn self k = Hmap.get k self.meta
+
+let get_header_int self h =
+ match get_header self h with
+ | Some x -> (try Some (int_of_string x) with _ -> None)
+ | None -> None
+
+let set_header k v self = { self with headers = Headers.set k v self.headers }
+let update_headers f self = { self with headers = f self.headers }
+let set_body b self = { self with body = b }
+
+(** Should we close the connection after this request? *)
+let close_after_req (self : _ t) : bool =
+ match self.http_version with
+ | 1, 1 -> get_header self "connection" = Some "close"
+ | 1, 0 -> not (get_header self "connection" = Some "keep-alive")
+ | _ -> false
+
+let pp_comp_ out comp =
+ Format.fprintf out "[%s]"
+ (String.concat ";" @@ List.map (Printf.sprintf "%S") comp)
+
+let pp_query out q =
+ Format.fprintf out "[%s]"
+ (String.concat ";" @@ List.map (fun (a, b) -> Printf.sprintf "%S,%S" a b) q)
+
+let pp_with ?(mask_header = fun _ -> false)
+ ?(headers_to_mask = [ "authorization"; "cookie" ]) ?(show_query = true)
+ ?(pp_body = fun out _ -> Format.pp_print_string out "?") () out self : unit
+ =
+ let pp_query out q =
+ if show_query then
+ pp_query out q
+ else
+ Format.fprintf out ""
+ in
+
+ let headers_to_mask = List.rev_map String.lowercase_ascii headers_to_mask in
+ (* hide some headers *)
+ let headers =
+ List.map
+ (fun (k, v) ->
+ let hidden = List.mem k headers_to_mask || mask_header k in
+ if hidden then
+ k, ""
+ else
+ k, v)
+ self.headers
+ in
+ Format.fprintf out
+ "{@[meth=%s;@ host=%s;@ headers=[@[%a@]];@ path=%S;@ body=%a;@ \
+ path_components=%a;@ query=%a@]}"
+ (Meth.to_string self.meth) self.host Headers.pp headers self.path pp_body
+ self.body pp_comp_ self.path_components pp_query self.query
+
+let pp_ out self : unit = pp_with () out self
+
+let pp out self : unit =
+ let pp_body out b = Format.fprintf out "%S" b in
+ pp_with ~pp_body () out self
+
+(* decode a "chunked" stream into a normal stream *)
+let read_stream_chunked_ ~bytes (bs : #IO.Input.t) : IO.Input.t =
+ Log.debug (fun k -> k "body: start reading chunked stream...");
+ IO.Input.read_chunked ~bytes ~fail:(fun s -> Bad_req (400, s)) bs
+
+let limit_body_size_ ~max_size ~bytes (bs : #IO.Input.t) : IO.Input.t =
+ Log.debug (fun k -> k "limit size of body to max-size=%d" max_size);
+ IO.Input.limit_size_to ~max_size ~close_rec:false ~bytes bs
+
+let limit_body_size ~max_size ~bytes (req : IO.Input.t t) : IO.Input.t t =
+ { req with body = limit_body_size_ ~max_size ~bytes req.body }
+
+(** read exactly [size] bytes from the stream *)
+let read_exactly ~size ~bytes (bs : #IO.Input.t) : IO.Input.t =
+ Log.debug (fun k -> k "body: must read exactly %d bytes" size);
+ IO.Input.reading_exactly bs ~close_rec:false ~bytes ~size
+
+(* parse request, but not body (yet) *)
+let parse_req_start ~client_addr ~get_time_s ~buf (bs : IO.Input.t) :
+ unit t option resp_result =
+ try
+ let line = IO.Input.read_line_using ~buf bs in
+ Log.debug (fun k -> k "parse request line: %s" line);
+ let start_time = get_time_s () in
+ let meth, path, version =
+ try
+ let off = ref 0 in
+ let meth = Parse_.word line off in
+ let path = Parse_.word line off in
+ let http_version = Parse_.word line off in
+ let version =
+ match http_version with
+ | "HTTP/1.1" -> 1
+ | "HTTP/1.0" -> 0
+ | v -> invalid_arg (spf "unsupported HTTP version: %s" v)
+ in
+ meth, path, version
+ with
+ | Invalid_argument msg ->
+ Log.error (fun k -> k "invalid request line: `%s`: %s" line msg);
+ raise (Bad_req (400, "Invalid request line"))
+ | _ ->
+ Log.error (fun k -> k "invalid request line: `%s`" line);
+ raise (Bad_req (400, "Invalid request line"))
+ in
+ let meth = Meth.of_string meth in
+ Log.debug (fun k -> k "got meth: %s, path %S" (Meth.to_string meth) path);
+ let headers = Headers.parse_ ~buf bs in
+ let host =
+ match Headers.get "Host" headers with
+ | None -> bad_reqf 400 "No 'Host' header in request"
+ | Some h -> h
+ in
+ let path_components, query = Util.split_query path in
+ let path_components = Util.split_on_slash path_components in
+ let query =
+ match Util.parse_query query with
+ | Ok l -> l
+ | Error e -> bad_reqf 400 "invalid query: %s" e
+ in
+ let req =
+ {
+ meth;
+ query;
+ host;
+ meta = Hmap.empty;
+ client_addr;
+ path;
+ path_components;
+ headers;
+ http_version = 1, version;
+ body = ();
+ start_time;
+ }
+ in
+ Ok (Some req)
+ with
+ | End_of_file | Sys_error _ | Unix.Unix_error _ -> Ok None
+ | Bad_req (c, s) -> Error (c, s)
+ | e -> Error (400, Printexc.to_string e)
+
+(* parse body, given the headers.
+ @param tr_stream a transformation of the input stream. *)
+let parse_body_ ~tr_stream ~bytes (req : IO.Input.t t) :
+ IO.Input.t t resp_result =
+ try
+ let size, has_size =
+ match Headers.get_exn "Content-Length" req.headers |> int_of_string with
+ | n -> n, true (* body of fixed size *)
+ | exception Not_found -> 0, false
+ | exception _ -> bad_reqf 400 "invalid content-length"
+ in
+ let body =
+ match get_header ~f:String.trim req "Transfer-Encoding" with
+ | None -> read_exactly ~size ~bytes @@ tr_stream req.body
+ | Some "chunked" when has_size ->
+ bad_reqf 400 "specifying both transfer-encoding and content-length"
+ | Some "chunked" ->
+ (* body sent by chunks *)
+ let bs : IO.Input.t =
+ read_stream_chunked_ ~bytes @@ tr_stream req.body
+ in
+ if size > 0 then (
+ (* TODO: ensure we recycle [bytes] when the new input is closed *)
+ let bytes = Bytes.create 4096 in
+ limit_body_size_ ~max_size:size ~bytes bs
+ ) else
+ bs
+ | Some s -> bad_reqf 500 "cannot handle transfer encoding: %s" s
+ in
+ Ok { req with body }
+ with
+ | End_of_file -> Error (400, "unexpected end of file")
+ | Bad_req (c, s) -> Error (c, s)
+ | e -> Error (400, Printexc.to_string e)
+
+let read_body_full ?bytes ?buf_size (self : IO.Input.t t) : string t =
+ try
+ let buf =
+ match bytes with
+ | Some b -> Buf.of_bytes b
+ | None -> Buf.create ?size:buf_size ()
+ in
+ let body = IO.Input.read_all_using ~buf self.body in
+ { self with body }
+ with
+ | Bad_req _ as e -> raise e
+ | e -> bad_reqf 500 "failed to read body: %s" (Printexc.to_string e)
+
+module Private_ = struct
+ let close_after_req = close_after_req
+ let parse_req_start = parse_req_start
+
+ let parse_req_start_exn ?(buf = Buf.create ()) ~client_addr ~get_time_s bs =
+ parse_req_start ~client_addr ~get_time_s ~buf bs |> unwrap_resp_result
+
+ let parse_body ?(bytes = Bytes.create 4096) req bs : _ t =
+ parse_body_ ~tr_stream:(fun s -> s) ~bytes { req with body = bs }
+ |> unwrap_resp_result
+
+ let[@inline] set_body body self = { self with body }
+end
diff --git a/src/core/request.mli b/src/core/request.mli
new file mode 100644
index 00000000..e4242bcf
--- /dev/null
+++ b/src/core/request.mli
@@ -0,0 +1,168 @@
+(** Requests
+
+ Requests are sent by a client, e.g. a web browser or cURL.
+ From the point of view of the server, they're inputs. *)
+
+open Common_
+
+type 'body t = private {
+ meth: Meth.t; (** HTTP method for this request. *)
+ host: string;
+ (** Host header, mandatory. It can also be found in {!headers}. *)
+ client_addr: Unix.sockaddr; (** Client address. Available since 0.14. *)
+ headers: Headers.t; (** List of headers. *)
+ mutable meta: Hmap.t; (** Metadata. @since NEXT_RELEASE *)
+ http_version: int * int;
+ (** HTTP version. This should be either [1, 0] or [1, 1]. *)
+ path: string; (** Full path of the requested URL. *)
+ path_components: string list;
+ (** Components of the path of the requested URL. *)
+ query: (string * string) list; (** Query part of the requested URL. *)
+ body: 'body; (** Body of the request. *)
+ start_time: float;
+ (** Obtained via [get_time_s] in {!create}
+ @since 0.11 *)
+}
+(** A request with method, path, host, headers, and a body, sent by a client.
+
+ The body is polymorphic because the request goes through
+ several transformations. First it has no body, as only the request
+ and headers are read; then it has a stream body; then the body might be
+ entirely read as a string via {!read_body_full}.
+
+ @since 0.6 The field [query] was added and contains the query parameters in ["?foo=bar,x=y"]
+ @since 0.6 The field [path_components] is the part of the path that precedes [query] and is split on ["/"].
+ @since 0.11 the type is a private alias
+ @since 0.11 the field [start_time] was added
+ *)
+
+val add_meta : _ t -> 'a Hmap.key -> 'a -> unit
+(** Add metadata
+ @since NEXT_RELEASE *)
+
+val get_meta : _ t -> 'a Hmap.key -> 'a option
+(** Get metadata
+ @since NEXT_RELEASE *)
+
+val get_meta_exn : _ t -> 'a Hmap.key -> 'a
+(** Like {!get_meta} but can fail
+ @raise Invalid_argument if not present
+ @since NEXT_RELEASE *)
+
+val pp_with :
+ ?mask_header:(string -> bool) ->
+ ?headers_to_mask:string list ->
+ ?show_query:bool ->
+ ?pp_body:(Format.formatter -> 'body -> unit) ->
+ unit ->
+ Format.formatter ->
+ 'body t ->
+ unit
+(** Pretty print the request. The exact format of this printing
+ is not specified.
+ @param mask_header function which is given each header name. If it
+ returns [true], the header's value is masked. The presence of
+ the header is still printed. Default [fun _ -> false].
+ @param headers_to_mask a list of headers masked by default.
+ Default is ["authorization"; "cookie"].
+ @show_query if [true] (default [true]), the query part of the
+ request is shown.
+ @param pp_body body printer (default prints "?" instead of the body,
+ which works even for stream bodies) *)
+
+val pp : Format.formatter -> string t -> unit
+(** Pretty print the request and its body. The exact format of this printing
+ is not specified. *)
+
+val pp_ : Format.formatter -> _ t -> unit
+(** Pretty print the request without its body. The exact format of this printing
+ is not specified. *)
+
+val headers : _ t -> Headers.t
+(** List of headers of the request, including ["Host"]. *)
+
+val get_header : ?f:(string -> string) -> _ t -> string -> string option
+(** [get_header req h] looks up header [h] in [req]. It returns [None] if the
+ header is not present. This is case insensitive and should be used
+ rather than looking up [h] verbatim in [headers]. *)
+
+val get_header_int : _ t -> string -> int option
+(** Same as {!get_header} but also performs a string to integer conversion. *)
+
+val set_header : string -> string -> 'a t -> 'a t
+(** [set_header k v req] sets [k: v] in the request [req]'s headers. *)
+
+val remove_header : string -> 'a t -> 'a t
+(** Remove one instance of this header.
+ @since NEXT_RELEASE *)
+
+val update_headers : (Headers.t -> Headers.t) -> 'a t -> 'a t
+(** Modify headers using the given function.
+ @since 0.11 *)
+
+val set_body : 'a -> _ t -> 'a t
+(** [set_body b req] returns a new query whose body is [b].
+ @since 0.11 *)
+
+val host : _ t -> string
+(** Host field of the request. It also appears in the headers. *)
+
+val client_addr : _ t -> Unix.sockaddr
+(** Client address of the request.
+ @since 0.16 *)
+
+val meth : _ t -> Meth.t
+(** Method for the request. *)
+
+val path : _ t -> string
+(** Request path. *)
+
+val query : _ t -> (string * string) list
+(** Decode the query part of the {!path} field.
+ @since 0.4 *)
+
+val body : 'b t -> 'b
+(** Request body, possibly empty. *)
+
+val start_time : _ t -> float
+(** time stamp (from {!Unix.gettimeofday}) after parsing the first line of the request
+ @since 0.11 *)
+
+val limit_body_size :
+ max_size:int -> bytes:bytes -> IO.Input.t t -> IO.Input.t t
+(** Limit the body size to [max_size] bytes, or return
+ a [413] error.
+ @since 0.3
+ *)
+
+val read_body_full : ?bytes:bytes -> ?buf_size:int -> IO.Input.t t -> string t
+(** Read the whole body into a string. Potentially blocking.
+
+ @param buf_size initial size of underlying buffer (since 0.11)
+ @param bytes the initial buffer (since 0.14)
+ *)
+
+(**/**)
+
+(* for internal usage, do not use. There is no guarantee of stability. *)
+module Private_ : sig
+ val parse_req_start :
+ client_addr:Unix.sockaddr ->
+ get_time_s:(unit -> float) ->
+ buf:Buf.t ->
+ IO.Input.t ->
+ unit t option resp_result
+
+ val parse_req_start_exn :
+ ?buf:Buf.t ->
+ client_addr:Unix.sockaddr ->
+ get_time_s:(unit -> float) ->
+ IO.Input.t ->
+ unit t option
+
+ val close_after_req : _ t -> bool
+ val parse_body : ?bytes:bytes -> unit t -> IO.Input.t -> IO.Input.t t
+ val set_body : 'a -> _ t -> 'a t
+end
+
+(**/**)
diff --git a/src/core/response.ml b/src/core/response.ml
new file mode 100644
index 00000000..e1d2721a
--- /dev/null
+++ b/src/core/response.ml
@@ -0,0 +1,164 @@
+open Common_
+
+type body =
+ [ `String of string | `Stream of IO.Input.t | `Writer of IO.Writer.t | `Void ]
+
+type t = { code: Response_code.t; headers: Headers.t; body: body }
+
+let set_body body self = { self with body }
+let set_headers headers self = { self with headers }
+let update_headers f self = { self with headers = f self.headers }
+let set_header k v self = { self with headers = Headers.set k v self.headers }
+let remove_header k self = { self with headers = Headers.remove k self.headers }
+let set_code code self = { self with code }
+
+let make_raw ?(headers = []) ~code body : t =
+ (* add content length to response *)
+ let headers =
+ Headers.set "Content-Length" (string_of_int (String.length body)) headers
+ in
+ { code; headers; body = `String body }
+
+let make_raw_stream ?(headers = []) ~code body : t =
+ let headers = Headers.set "Transfer-Encoding" "chunked" headers in
+ { code; headers; body = `Stream body }
+
+let make_raw_writer ?(headers = []) ~code body : t =
+ let headers = Headers.set "Transfer-Encoding" "chunked" headers in
+ { code; headers; body = `Writer body }
+
+let make_void_force_ ?(headers = []) ~code () : t =
+ { code; headers; body = `Void }
+
+let make_void ?(headers = []) ~code () : t =
+ let is_ok = code < 200 || code = 204 || code = 304 in
+ if is_ok then
+ make_void_force_ ~headers ~code ()
+ else
+ make_raw ~headers ~code "" (* invalid to not have a body *)
+
+let make_string ?headers ?(code = 200) r =
+ match r with
+ | Ok body -> make_raw ?headers ~code body
+ | Error (code, msg) -> make_raw ?headers ~code msg
+
+let make_stream ?headers ?(code = 200) r =
+ match r with
+ | Ok body -> make_raw_stream ?headers ~code body
+ | Error (code, msg) -> make_raw ?headers ~code msg
+
+let make_writer ?headers ?(code = 200) r : t =
+ match r with
+ | Ok body -> make_raw_writer ?headers ~code body
+ | Error (code, msg) -> make_raw ?headers ~code msg
+
+let make ?headers ?(code = 200) r : t =
+ match r with
+ | Ok (`String body) -> make_raw ?headers ~code body
+ | Ok (`Stream body) -> make_raw_stream ?headers ~code body
+ | Ok `Void -> make_void ?headers ~code ()
+ | Ok (`Writer f) -> make_raw_writer ?headers ~code f
+ | Error (code, msg) -> make_raw ?headers ~code msg
+
+let fail ?headers ~code fmt =
+ Printf.ksprintf (fun msg -> make_raw ?headers ~code msg) fmt
+
+exception Bad_req = Bad_req
+
+let fail_raise ~code fmt =
+ Printf.ksprintf (fun msg -> raise (Bad_req (code, msg))) fmt
+
+let pp out self : unit =
+ let pp_body out = function
+ | `String s -> Format.fprintf out "%S" s
+ | `Stream _ -> Format.pp_print_string out ""
+ | `Writer _ -> Format.pp_print_string out ""
+ | `Void -> ()
+ in
+ Format.fprintf out "{@[code=%d;@ headers=[@[%a@]];@ body=%a@]}" self.code
+ Headers.pp self.headers pp_body self.body
+
+let output_ ~bytes (oc : IO.Output.t) (self : t) : unit =
+ (* double indirection:
+ - print into [buffer] using [bprintf]
+ - transfer to [buf_] so we can output from there *)
+ let tmp_buffer = Buffer.create 32 in
+ let buf = Buf.of_bytes bytes in
+
+ (* write start of reply *)
+ Printf.bprintf tmp_buffer "HTTP/1.1 %d %s\r\n" self.code
+ (Response_code.descr self.code);
+ Buf.add_buffer buf tmp_buffer;
+ Buffer.clear tmp_buffer;
+
+ let body, is_chunked =
+ match self.body with
+ | `String s when String.length s > 1024 * 500 ->
+ (* chunk-encode large bodies *)
+ `Writer (IO.Writer.of_string s), true
+ | `String _ as b -> b, false
+ | `Stream _ as b -> b, true
+ | `Writer _ as b -> b, true
+ | `Void as b -> b, false
+ in
+ let headers =
+ if is_chunked then
+ self.headers
+ |> Headers.set "transfer-encoding" "chunked"
+ |> Headers.remove "content-length"
+ else
+ self.headers
+ in
+ let self = { self with headers; body } in
+ Log.debug (fun k ->
+ k "t[%d]: output response: %s"
+ (Thread.id @@ Thread.self ())
+ (Format.asprintf "%a" pp { self with body = `String "<...>" }));
+
+ (* write headers, using [buf] to batch writes *)
+ List.iter
+ (fun (k, v) ->
+ Printf.bprintf tmp_buffer "%s: %s\r\n" k v;
+ Buf.add_buffer buf tmp_buffer;
+ Buffer.clear tmp_buffer)
+ headers;
+
+ IO.Output.output_buf oc buf;
+ IO.Output.output_string oc "\r\n";
+ Buf.clear buf;
+
+ (match body with
+ | `String "" | `Void -> ()
+ | `String s -> IO.Output.output_string oc s
+ | `Writer w ->
+ (* use buffer to chunk encode [w] *)
+ let oc' = IO.Output.chunk_encoding ~buf ~close_rec:false oc in
+ (try
+ IO.Writer.write oc' w;
+ IO.Output.close oc'
+ with e ->
+ let bt = Printexc.get_raw_backtrace () in
+ IO.Output.close oc';
+ IO.Output.flush oc;
+ Printexc.raise_with_backtrace e bt)
+ | `Stream str ->
+ (match IO.Input.output_chunked' ~buf oc str with
+ | () ->
+ Log.debug (fun k ->
+ k "t[%d]: done outputing stream" (Thread.id @@ Thread.self ()));
+ IO.Input.close str
+ | exception e ->
+ let bt = Printexc.get_raw_backtrace () in
+ Log.error (fun k ->
+ k "t[%d]: outputing stream failed with %s"
+ (Thread.id @@ Thread.self ())
+ (Printexc.to_string e));
+ IO.Input.close str;
+ IO.Output.flush oc;
+ Printexc.raise_with_backtrace e bt));
+ IO.Output.flush oc
+
+module Private_ = struct
+ let make_void_force_ = make_void_force_
+ let output_ = output_
+end
diff --git a/src/core/response.mli b/src/core/response.mli
new file mode 100644
index 00000000..4cf0f192
--- /dev/null
+++ b/src/core/response.mli
@@ -0,0 +1,122 @@
+(** Responses
+
+ Responses are what a http server, such as {!Tiny_httpd}, send back to
+ the client to answer a {!Request.t}*)
+
+type body =
+ [ `String of string | `Stream of IO.Input.t | `Writer of IO.Writer.t | `Void ]
+(** Body of a response, either as a simple string,
+ or a stream of bytes, or nothing (for server-sent events notably).
+
+ - [`String str] replies with a body set to this string, and a known content-length.
+ - [`Stream str] replies with a body made from this string, using chunked encoding.
+ - [`Void] replies with no body.
+ - [`Writer w] replies with a body created by the writer [w], using
+ a chunked encoding.
+ It is available since 0.14.
+ *)
+
+type t = private {
+ 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. *)
+ body: body; (** Body of the response. Can be empty. *)
+}
+(** A response to send back to a client. *)
+
+val set_body : body -> t -> t
+(** Set the body of the response.
+ @since 0.11 *)
+
+val set_header : string -> string -> t -> t
+(** Set a header.
+ @since 0.11 *)
+
+val update_headers : (Headers.t -> Headers.t) -> t -> t
+(** Modify headers.
+ @since 0.11 *)
+
+val remove_header : string -> t -> t
+(** Remove one instance of this header.
+ @since NEXT_RELEASE *)
+
+val set_headers : Headers.t -> t -> t
+(** Set all headers.
+ @since 0.11 *)
+
+val set_code : Response_code.t -> t -> t
+(** Set the response code.
+ @since 0.11 *)
+
+val make_raw : ?headers:Headers.t -> code:Response_code.t -> string -> t
+(** Make a response from its raw components, with a string body.
+ Use [""] to not send a body at all. *)
+
+val make_raw_stream :
+ ?headers:Headers.t -> code:Response_code.t -> IO.Input.t -> t
+(** Same as {!make_raw} but with a stream body. The body will be sent with
+ the chunked transfer-encoding. *)
+
+val make_void : ?headers:Headers.t -> code:int -> unit -> t
+(** Return a response without a body at all.
+ @since 0.13 *)
+
+val make :
+ ?headers:Headers.t ->
+ ?code:int ->
+ (body, Response_code.t * string) result ->
+ t
+(** [make r] turns a result into a response.
+
+ - [make (Ok body)] replies with [200] and the body.
+ - [make (Error (code,msg))] replies with the given error code
+ and message as body.
+ *)
+
+val make_string :
+ ?headers:Headers.t ->
+ ?code:int ->
+ (string, Response_code.t * string) result ->
+ t
+(** Same as {!make} but with a string body. *)
+
+val make_writer :
+ ?headers:Headers.t ->
+ ?code:int ->
+ (IO.Writer.t, Response_code.t * string) result ->
+ t
+(** Same as {!make} but with a writer body. *)
+
+val make_stream :
+ ?headers:Headers.t ->
+ ?code:int ->
+ (IO.Input.t, Response_code.t * string) result ->
+ t
+(** Same as {!make} but with a stream body. *)
+
+val fail : ?headers:Headers.t -> code:int -> ('a, unit, string, t) format4 -> 'a
+(** Make the current request fail with the given code and message.
+ Example: [fail ~code:404 "oh noes, %s not found" "waldo"].
+ *)
+
+exception Bad_req of int * string
+(** Exception raised by {!fail_raise} with the HTTP code and body *)
+
+val fail_raise : code:int -> ('a, unit, string, 'b) format4 -> 'a
+(** Similar to {!fail} but raises an exception that exits the current handler.
+ This should not be used outside of a (path) handler.
+ Example: [fail_raise ~code:404 "oh noes, %s not found" "waldo"; never_executed()]
+ @raise Bad_req always
+ *)
+
+val pp : Format.formatter -> t -> unit
+(** Pretty print the response. The exact format is not specified. *)
+
+(**/**)
+
+module Private_ : sig
+ val make_void_force_ : ?headers:Headers.t -> code:int -> unit -> t
+ val output_ : bytes:Bytes.t -> IO.Output.t -> t -> unit
+end
+
+(**/**)
diff --git a/src/core/response_code.ml b/src/core/response_code.ml
new file mode 100644
index 00000000..cc97380d
--- /dev/null
+++ b/src/core/response_code.ml
@@ -0,0 +1,32 @@
+type t = int
+
+let ok = 200
+let not_found = 404
+
+let descr = function
+ | 100 -> "Continue"
+ | 200 -> "OK"
+ | 201 -> "Created"
+ | 202 -> "Accepted"
+ | 204 -> "No content"
+ | 300 -> "Multiple choices"
+ | 301 -> "Moved permanently"
+ | 302 -> "Found"
+ | 304 -> "Not Modified"
+ | 400 -> "Bad request"
+ | 401 -> "Unauthorized"
+ | 403 -> "Forbidden"
+ | 404 -> "Not found"
+ | 405 -> "Method not allowed"
+ | 408 -> "Request timeout"
+ | 409 -> "Conflict"
+ | 410 -> "Gone"
+ | 411 -> "Length required"
+ | 413 -> "Payload too large"
+ | 417 -> "Expectation failed"
+ | 500 -> "Internal server error"
+ | 501 -> "Not implemented"
+ | 503 -> "Service unavailable"
+ | n -> "Unknown response code " ^ string_of_int n (* TODO *)
+
+let[@inline] is_success n = n >= 200 && n < 400
diff --git a/src/core/response_code.mli b/src/core/response_code.mli
new file mode 100644
index 00000000..fd0663d4
--- /dev/null
+++ b/src/core/response_code.mli
@@ -0,0 +1,20 @@
+(** Response Codes *)
+
+type t = int
+(** A standard HTTP code.
+
+ https://tools.ietf.org/html/rfc7231#section-6 *)
+
+val ok : t
+(** The code [200] *)
+
+val not_found : t
+(** The code [404] *)
+
+val descr : t -> string
+(** A description of some of the error codes.
+ NOTE: this is not complete (yet). *)
+
+val is_success : t -> bool
+(** [is_success code] is true iff [code] is in the [2xx] or [3xx] range.
+ @since NEXT_RELEASE *)
diff --git a/src/core/route.ml b/src/core/route.ml
new file mode 100644
index 00000000..f2e52f08
--- /dev/null
+++ b/src/core/route.ml
@@ -0,0 +1,93 @@
+type path = string list (* split on '/' *)
+
+type (_, _) comp =
+ | Exact : string -> ('a, 'a) comp
+ | Int : (int -> 'a, 'a) comp
+ | String : (string -> 'a, 'a) comp
+ | String_urlencoded : (string -> 'a, 'a) comp
+
+type (_, _) t =
+ | Fire : ('b, 'b) t
+ | Rest : { url_encoded: bool } -> (string -> 'b, 'b) t
+ | Compose : ('a, 'b) comp * ('b, 'c) t -> ('a, 'c) t
+
+let return = Fire
+let rest_of_path = Rest { url_encoded = false }
+let rest_of_path_urlencoded = Rest { url_encoded = true }
+let ( @/ ) a b = Compose (a, b)
+let string = String
+let string_urlencoded = String_urlencoded
+let int = Int
+let exact (s : string) = Exact s
+
+let exact_path (s : string) tail =
+ let rec fn = function
+ | [] -> tail
+ | "" :: ls -> fn ls
+ | s :: ls -> exact s @/ fn ls
+ in
+ fn (String.split_on_char '/' s)
+
+let rec eval : type a b. path -> (a, b) t -> a -> b option =
+ fun path route f ->
+ match path, route with
+ | [], Fire -> Some f
+ | _, Fire -> None
+ | _, Rest { url_encoded } ->
+ let whole_path = String.concat "/" path in
+ (match
+ if url_encoded then (
+ match Util.percent_decode whole_path with
+ | Some s -> s
+ | None -> raise_notrace Exit
+ ) else
+ whole_path
+ with
+ | whole_path -> Some (f whole_path)
+ | exception Exit -> None)
+ | c1 :: path', Compose (comp, route') ->
+ (match comp with
+ | Int ->
+ (match int_of_string c1 with
+ | i -> eval path' route' (f i)
+ | exception _ -> None)
+ | String -> eval path' route' (f c1)
+ | String_urlencoded ->
+ (match Util.percent_decode c1 with
+ | None -> None
+ | Some s -> eval path' route' (f s))
+ | Exact s ->
+ if s = c1 then
+ eval path' route' f
+ else
+ None)
+ | [], Compose (String, Fire) -> Some (f "") (* trailing *)
+ | [], Compose (String_urlencoded, Fire) -> Some (f "") (* trailing *)
+ | [], Compose _ -> None
+
+let bpf = Printf.bprintf
+
+let rec pp_ : type a b. Buffer.t -> (a, b) t -> unit =
+ fun out -> function
+ | Fire -> bpf out "/"
+ | Rest { url_encoded } ->
+ bpf out ""
+ (if url_encoded then
+ "_urlencoded"
+ else
+ "")
+ | Compose (Exact s, tl) -> bpf out "%s/%a" s pp_ tl
+ | Compose (Int, tl) -> bpf out "/%a" pp_ tl
+ | Compose (String, tl) -> bpf out "/%a" pp_ tl
+ | Compose (String_urlencoded, tl) -> bpf out "/%a" pp_ tl
+
+let to_string x =
+ let b = Buffer.create 16 in
+ pp_ b x;
+ Buffer.contents b
+
+module Private_ = struct
+ let eval = eval
+end
+
+let pp out x = Format.pp_print_string out (to_string x)
diff --git a/src/core/route.mli b/src/core/route.mli
new file mode 100644
index 00000000..4df45aba
--- /dev/null
+++ b/src/core/route.mli
@@ -0,0 +1,58 @@
+(** Routing
+
+ Basic type-safe routing of handlers based on URL paths. This is optional,
+ it is possible to only define the root handler with something like
+ {{: https://github.com/anuragsoni/routes/} Routes}.
+ @since 0.6 *)
+
+type ('a, 'b) comp
+(** An atomic component of a path *)
+
+type ('a, 'b) t
+(** A route, composed of path components *)
+
+val int : (int -> 'a, 'a) comp
+(** Matches an integer. *)
+
+val string : (string -> 'a, 'a) comp
+(** Matches a string not containing ['/'] and binds it as is. *)
+
+val string_urlencoded : (string -> 'a, 'a) comp
+(** Matches a URL-encoded string, and decodes it. *)
+
+val exact : string -> ('a, 'a) comp
+(** [exact "s"] matches ["s"] and nothing else. *)
+
+val return : ('a, 'a) t
+(** Matches the empty path. *)
+
+val rest_of_path : (string -> 'a, 'a) t
+(** Matches a string, even containing ['/']. This will match
+ the entirety of the remaining route.
+ @since 0.7 *)
+
+val rest_of_path_urlencoded : (string -> 'a, 'a) t
+(** Matches a string, even containing ['/'], an URL-decode it.
+ This will match the entirety of the remaining route.
+ @since 0.7 *)
+
+val ( @/ ) : ('a, 'b) comp -> ('b, 'c) t -> ('a, 'c) t
+(** [comp / route] matches ["foo/bar/…"] iff [comp] matches ["foo"],
+ and [route] matches ["bar/…"]. *)
+
+val exact_path : string -> ('a, 'b) t -> ('a, 'b) t
+(** [exact_path "foo/bar/..." r] is equivalent to
+ [exact "foo" @/ exact "bar" @/ ... @/ r]
+ @since 0.11 **)
+
+val pp : Format.formatter -> _ t -> unit
+(** Print the route.
+ @since 0.7 *)
+
+val to_string : _ t -> string
+(** Print the route.
+ @since 0.7 *)
+
+module Private_ : sig
+ val eval : string list -> ('a, 'b) t -> 'a -> 'b option
+end
diff --git a/src/core/server.ml b/src/core/server.ml
new file mode 100644
index 00000000..1eb1715b
--- /dev/null
+++ b/src/core/server.ml
@@ -0,0 +1,514 @@
+open Common_
+
+type resp_error = Response_code.t * string
+
+module Middleware = struct
+ type handler = IO.Input.t Request.t -> resp:(Response.t -> unit) -> unit
+ type t = handler -> handler
+
+ (** Apply a list of middlewares to [h] *)
+ let apply_l (l : t list) (h : handler) : handler =
+ List.fold_right (fun m h -> m h) l h
+
+ let[@inline] nil : t = fun h -> h
+end
+
+(* a request handler. handles a single request. *)
+type cb_path_handler = IO.Output.t -> Middleware.handler
+
+module type SERVER_SENT_GENERATOR = sig
+ val set_headers : Headers.t -> unit
+
+ val send_event :
+ ?event:string -> ?id:string -> ?retry:string -> data:string -> unit -> unit
+
+ val close : unit -> unit
+end
+
+type server_sent_generator = (module SERVER_SENT_GENERATOR)
+
+(** Handler that upgrades to another protocol *)
+module type UPGRADE_HANDLER = sig
+ type handshake_state
+ (** Some specific state returned after handshake *)
+
+ val name : string
+ (** Name in the "upgrade" header *)
+
+ val handshake : unit Request.t -> (Headers.t * handshake_state, string) result
+ (** Perform the handshake and upgrade the connection. The returned
+ code is [101] alongside these headers. *)
+
+ val handle_connection :
+ Unix.sockaddr -> handshake_state -> IO.Input.t -> IO.Output.t -> unit
+ (** Take control of the connection and take it from there *)
+end
+
+type upgrade_handler = (module UPGRADE_HANDLER)
+
+exception Upgrade of unit Request.t * upgrade_handler
+
+module type IO_BACKEND = sig
+ val init_addr : unit -> string
+ val init_port : unit -> int
+
+ val get_time_s : unit -> float
+ (** obtain the current timestamp in seconds. *)
+
+ val tcp_server : unit -> IO.TCP_server.builder
+ (** Server that can listen on a port and handle clients. *)
+end
+
+type handler_result =
+ | Handle of (int * Middleware.t) list * cb_path_handler
+ | Fail of resp_error
+ | Upgrade of upgrade_handler
+
+let unwrap_handler_result req = function
+ | Handle (l, h) -> l, h
+ | Fail (c, s) -> raise (Bad_req (c, s))
+ | Upgrade up -> raise (Upgrade (req, up))
+
+type t = {
+ backend: (module IO_BACKEND);
+ mutable tcp_server: IO.TCP_server.t option;
+ mutable handler: IO.Input.t Request.t -> Response.t;
+ (** toplevel handler, if any *)
+ mutable middlewares: (int * Middleware.t) list; (** Global middlewares *)
+ mutable middlewares_sorted: (int * Middleware.t) list lazy_t;
+ (** sorted version of {!middlewares} *)
+ mutable path_handlers: (unit Request.t -> handler_result option) list;
+ (** path handlers *)
+ bytes_pool: bytes Pool.t;
+}
+
+let addr (self : t) =
+ match self.tcp_server with
+ | None ->
+ let (module B) = self.backend in
+ B.init_addr ()
+ | Some s -> fst @@ s.endpoint ()
+
+let port (self : t) =
+ match self.tcp_server with
+ | None ->
+ let (module B) = self.backend in
+ B.init_port ()
+ | Some s -> snd @@ s.endpoint ()
+
+let active_connections (self : t) =
+ match self.tcp_server with
+ | None -> 0
+ | Some s -> s.active_connections ()
+
+let sort_middlewares_ l =
+ List.stable_sort (fun (s1, _) (s2, _) -> compare s1 s2) l
+
+let add_middleware ~stage self m =
+ let stage =
+ match stage with
+ | `Encoding -> 0
+ | `Stage n when n < 1 -> invalid_arg "add_middleware: bad stage"
+ | `Stage n -> n
+ in
+ self.middlewares <- (stage, m) :: self.middlewares;
+ self.middlewares_sorted <- lazy (sort_middlewares_ self.middlewares)
+
+let add_decode_request_cb self f =
+ (* turn it into a middleware *)
+ let m h req ~resp =
+ (* see if [f] modifies the stream *)
+ let req0 = Request.Private_.set_body () req in
+ match f req0 with
+ | None -> h req ~resp (* pass through *)
+ | Some (req1, tr_stream) ->
+ let body = tr_stream req.Request.body in
+ let req = Request.set_body body req1 in
+ h req ~resp
+ in
+ add_middleware self ~stage:`Encoding m
+
+let add_encode_response_cb self f =
+ let m h req ~resp =
+ h req ~resp:(fun r ->
+ let req0 = Request.Private_.set_body () req in
+ (* now transform [r] if we want to *)
+ match f req0 r with
+ | None -> resp r
+ | Some r' -> resp r')
+ in
+ add_middleware self ~stage:`Encoding m
+
+let set_top_handler self f = self.handler <- f
+
+(* route the given handler.
+ @param tr_req wraps the actual concrete function returned by the route
+ and makes it into a handler. *)
+let add_route_handler_ ?(accept = fun _req -> Ok ()) ?(middlewares = []) ?meth
+ ~tr_req self (route : _ Route.t) f =
+ let middlewares = List.map (fun h -> 5, h) middlewares in
+ let ph req : handler_result option =
+ match meth with
+ | Some m when m <> req.Request.meth -> None (* ignore *)
+ | _ ->
+ (match Route.Private_.eval req.Request.path_components route f with
+ | Some handler ->
+ (* we have a handler, do we accept the request based on its headers? *)
+ (match accept req with
+ | Ok () ->
+ Some
+ (Handle
+ (middlewares, fun oc req ~resp -> tr_req oc req ~resp handler))
+ | Error err -> Some (Fail err))
+ | None -> None (* path didn't match *))
+ in
+ self.path_handlers <- ph :: self.path_handlers
+
+let add_route_handler (type a) ?accept ?middlewares ?meth self
+ (route : (a, _) Route.t) (f : _) : unit =
+ let tr_req _oc req ~resp f =
+ let req =
+ Pool.with_resource self.bytes_pool @@ fun bytes ->
+ Request.read_body_full ~bytes req
+ in
+ resp (f req)
+ in
+ add_route_handler_ ?accept ?middlewares ?meth self route ~tr_req f
+
+let add_route_handler_stream ?accept ?middlewares ?meth self route f =
+ let tr_req _oc req ~resp f = resp (f req) in
+ add_route_handler_ ?accept ?middlewares ?meth self route ~tr_req f
+
+let[@inline] _opt_iter ~f o =
+ match o with
+ | None -> ()
+ | Some x -> f x
+
+exception Exit_SSE
+
+let add_route_server_sent_handler ?accept self route f =
+ let tr_req (oc : IO.Output.t) req ~resp f =
+ let req =
+ Pool.with_resource self.bytes_pool @@ fun bytes ->
+ Request.read_body_full ~bytes req
+ in
+ let headers =
+ ref Headers.(empty |> set "content-type" "text/event-stream")
+ in
+
+ (* send response once *)
+ let resp_sent = ref false in
+ let send_response_idempotent_ () =
+ if not !resp_sent then (
+ resp_sent := true;
+ (* send 200 response now *)
+ let initial_resp =
+ Response.Private_.make_void_force_ ~headers:!headers ~code:200 ()
+ in
+ resp initial_resp
+ )
+ in
+
+ let[@inline] writef fmt =
+ Printf.ksprintf (IO.Output.output_string oc) fmt
+ in
+
+ let send_event ?event ?id ?retry ~data () : unit =
+ send_response_idempotent_ ();
+ _opt_iter event ~f:(fun e -> writef "event: %s\n" e);
+ _opt_iter id ~f:(fun e -> writef "id: %s\n" e);
+ _opt_iter retry ~f:(fun e -> writef "retry: %s\n" e);
+ let l = String.split_on_char '\n' data in
+ List.iter (fun s -> writef "data: %s\n" s) l;
+ IO.Output.output_string oc "\n";
+ (* finish group *)
+ IO.Output.flush oc
+ in
+ let module SSG = struct
+ let set_headers h =
+ if not !resp_sent then (
+ headers := List.rev_append h !headers;
+ send_response_idempotent_ ()
+ )
+
+ let send_event = send_event
+ let close () = raise Exit_SSE
+ end in
+ (try f req (module SSG : SERVER_SENT_GENERATOR)
+ with Exit_SSE -> IO.Output.close oc);
+ Log.info (fun k -> k "closed SSE connection")
+ in
+ add_route_handler_ self ?accept ~meth:`GET route ~tr_req f
+
+let add_upgrade_handler ?(accept = fun _ -> Ok ()) (self : t) route f : unit =
+ let ph req : handler_result option =
+ if req.Request.meth <> `GET then
+ None
+ else (
+ match accept req with
+ | Ok () ->
+ (match Route.Private_.eval req.Request.path_components route f with
+ | Some up -> Some (Upgrade up)
+ | None -> None (* path didn't match *))
+ | Error err -> Some (Fail err)
+ )
+ in
+ self.path_handlers <- ph :: self.path_handlers
+
+let clear_bytes_ bs = Bytes.fill bs 0 (Bytes.length bs) '\x00'
+
+let create_from ?(buf_size = 16 * 1_024) ?(middlewares = []) ~backend () : t =
+ let handler _req = Response.fail ~code:404 "no top handler" in
+ let self =
+ {
+ backend;
+ tcp_server = None;
+ handler;
+ path_handlers = [];
+ middlewares = [];
+ middlewares_sorted = lazy [];
+ bytes_pool =
+ Pool.create ~clear:clear_bytes_
+ ~mk_item:(fun () -> Bytes.create buf_size)
+ ();
+ }
+ in
+ List.iter (fun (stage, m) -> add_middleware self ~stage m) middlewares;
+ self
+
+let stop (self : t) =
+ match self.tcp_server with
+ | None -> ()
+ | Some s -> s.stop ()
+
+let running (self : t) =
+ match self.tcp_server with
+ | None -> false
+ | Some s -> s.running ()
+
+let find_map f l =
+ let rec aux f = function
+ | [] -> None
+ | x :: l' ->
+ (match f x with
+ | Some _ as res -> res
+ | None -> aux f l')
+ in
+ aux f l
+
+let string_as_list_contains_ (s : string) (sub : string) : bool =
+ let fragments = String.split_on_char ',' s in
+ List.exists (fun fragment -> String.trim fragment = sub) fragments
+
+(* handle client on [ic] and [oc] *)
+let client_handle_for (self : t) ~client_addr ic oc : unit =
+ Pool.with_resource self.bytes_pool @@ fun bytes_req ->
+ Pool.with_resource self.bytes_pool @@ fun bytes_res ->
+ let (module B) = self.backend in
+
+ (* how to log the response to this query *)
+ let log_response (req : _ Request.t) (resp : Response.t) =
+ if not Log.dummy then (
+ let msgf k =
+ let elapsed = B.get_time_s () -. req.start_time in
+ k
+ ("response to=%s code=%d time=%.3fs meth=%s path=%S" : _ format4)
+ (Util.show_sockaddr client_addr)
+ resp.code elapsed (Meth.to_string req.meth) req.path
+ in
+ if Response_code.is_success resp.code then
+ Log.info msgf
+ else
+ Log.error msgf
+ )
+ in
+
+ let log_exn msg bt =
+ Log.error (fun k ->
+ k "error while processing response for %s msg=%s@.%s"
+ (Util.show_sockaddr client_addr)
+ msg
+ (Printexc.raw_backtrace_to_string bt))
+ in
+
+ (* handle generic exception *)
+ let handle_exn e bt : unit =
+ let msg = Printexc.to_string e in
+ let resp = Response.fail ~code:500 "server error: %s" msg in
+ if not Log.dummy then log_exn msg bt;
+ Response.Private_.output_ ~bytes:bytes_res oc resp
+ in
+
+ let handle_bad_req req e bt =
+ let msg = Printexc.to_string e in
+ let resp = Response.fail ~code:500 "server error: %s" msg in
+ if not Log.dummy then (
+ log_exn msg bt;
+ log_response req resp
+ );
+ Response.Private_.output_ ~bytes:bytes_res oc resp
+ in
+
+ let handle_upgrade req (module UP : UPGRADE_HANDLER) : unit =
+ Log.debug (fun k -> k "upgrade connection");
+ try
+ (* check headers *)
+ (match Request.get_header req "connection" with
+ | Some str when string_as_list_contains_ str "Upgrade" -> ()
+ | _ -> bad_reqf 426 "connection header must contain 'Upgrade'");
+ (match Request.get_header req "upgrade" with
+ | Some u when u = UP.name -> ()
+ | Some u -> bad_reqf 426 "expected upgrade to be '%s', got '%s'" UP.name u
+ | None -> bad_reqf 426 "expected 'connection: upgrade' header");
+
+ (* ok, this is the upgrade we expected *)
+ match UP.handshake req with
+ | Error msg ->
+ (* fail the upgrade *)
+ Log.error (fun k -> k "upgrade failed: %s" msg);
+ let resp = Response.make_raw ~code:429 "upgrade required" in
+ log_response req resp;
+ Response.Private_.output_ ~bytes:bytes_res oc resp
+ | Ok (headers, handshake_st) ->
+ (* send the upgrade reply *)
+ let headers =
+ [ "connection", "upgrade"; "upgrade", UP.name ] @ headers
+ in
+ let resp = Response.make_string ~code:101 ~headers (Ok "") in
+ log_response req resp;
+ Response.Private_.output_ ~bytes:bytes_res oc resp;
+
+ UP.handle_connection client_addr handshake_st ic oc
+ with e ->
+ let bt = Printexc.get_raw_backtrace () in
+ handle_bad_req req e bt
+ in
+
+ let continue = ref true in
+
+ let handle_one_req () =
+ match
+ let buf = Buf.of_bytes bytes_req in
+ Request.Private_.parse_req_start ~client_addr ~get_time_s:B.get_time_s
+ ~buf ic
+ with
+ | Ok None -> continue := false (* client is done *)
+ | Error (c, s) ->
+ (* connection error, close *)
+ let res = Response.make_raw ~code:c s in
+ (try Response.Private_.output_ ~bytes:bytes_res oc res
+ with Sys_error _ -> ());
+ continue := false
+ | Ok (Some req) ->
+ Log.debug (fun k ->
+ k "t[%d]: parsed request: %s"
+ (Thread.id @@ Thread.self ())
+ (Format.asprintf "@[%a@]" Request.pp_ req));
+
+ if Request.Private_.close_after_req req then continue := false;
+
+ (try
+ (* is there a handler for this path? *)
+ let handler_middlewares, base_handler =
+ match find_map (fun ph -> ph req) self.path_handlers with
+ | Some f -> unwrap_handler_result req f
+ | None -> [], fun _oc req ~resp -> resp (self.handler req)
+ in
+
+ (* handle expect/continue *)
+ (match Request.get_header ~f:String.trim req "Expect" with
+ | Some "100-continue" ->
+ Log.debug (fun k -> k "send back: 100 CONTINUE");
+ Response.Private_.output_ ~bytes:bytes_res oc
+ (Response.make_raw ~code:100 "")
+ | Some s -> bad_reqf 417 "unknown expectation %s" s
+ | None -> ());
+
+ (* merge per-request middlewares with the server-global middlewares *)
+ let global_middlewares = Lazy.force self.middlewares_sorted in
+ let all_middlewares =
+ if handler_middlewares = [] then
+ global_middlewares
+ else
+ sort_middlewares_
+ (List.rev_append handler_middlewares self.middlewares)
+ in
+
+ (* apply middlewares *)
+ let handler oc =
+ List.fold_right
+ (fun (_, m) h -> m h)
+ all_middlewares (base_handler oc)
+ in
+
+ (* now actually read request's body into a stream *)
+ let req = Request.Private_.parse_body ~bytes:bytes_req req ic in
+
+ (* how to reply *)
+ let resp r =
+ try
+ if Headers.get "connection" r.Response.headers = Some "close" then
+ continue := false;
+ log_response req r;
+ Response.Private_.output_ ~bytes:bytes_res oc r
+ with Sys_error e ->
+ Log.debug (fun k ->
+ k "error when writing response: %s@.connection broken" e);
+ continue := false
+ in
+
+ (* call handler *)
+ try handler oc req ~resp
+ with Sys_error e ->
+ Log.debug (fun k ->
+ k "error while handling request: %s@.connection broken" e);
+ continue := false
+ with
+ | Sys_error e ->
+ (* connection broken somehow *)
+ Log.debug (fun k -> k "error: %s@. connection broken" e);
+ continue := false
+ | Bad_req (code, s) ->
+ continue := false;
+ let resp = Response.make_raw ~code s in
+ log_response req resp;
+ Response.Private_.output_ ~bytes:bytes_res oc resp
+ | Upgrade _ as e -> raise e
+ | e ->
+ let bt = Printexc.get_raw_backtrace () in
+ handle_bad_req req e bt)
+ in
+
+ try
+ while !continue && running self do
+ Log.debug (fun k ->
+ k "t[%d]: read next request" (Thread.id @@ Thread.self ()));
+ handle_one_req ()
+ done
+ with
+ | Upgrade (req, up) ->
+ (* upgrades take over the whole connection, we won't process
+ any further request *)
+ handle_upgrade req up
+ | e ->
+ let bt = Printexc.get_raw_backtrace () in
+ handle_exn e bt
+
+let client_handler (self : t) : IO.TCP_server.conn_handler =
+ { IO.TCP_server.handle = client_handle_for self }
+
+let is_ipv6 (self : t) =
+ let (module B) = self.backend in
+ Util.is_ipv6_str (B.init_addr ())
+
+let run_exn ?(after_init = ignore) (self : t) : unit =
+ let (module B) = self.backend in
+ let server = B.tcp_server () in
+ server.serve
+ ~after_init:(fun tcp_server ->
+ self.tcp_server <- Some tcp_server;
+ after_init ())
+ ~handle:(client_handler self) ()
+
+let run ?after_init self : _ result =
+ try Ok (run_exn ?after_init self) with e -> Error e
diff --git a/src/core/server.mli b/src/core/server.mli
new file mode 100644
index 00000000..e856c7e4
--- /dev/null
+++ b/src/core/server.mli
@@ -0,0 +1,298 @@
+(** HTTP server.
+
+ This module implements a very simple, basic HTTP/1.1 server using blocking
+ IOs and threads.
+
+ It is possible to use a thread pool, see {!create}'s argument [new_thread].
+
+ @since 0.13
+*)
+
+(** {2 Middlewares}
+
+ A middleware can be inserted in a handler to modify or observe
+ its behavior.
+
+ @since 0.11
+*)
+
+module Middleware : sig
+ type handler = IO.Input.t Request.t -> resp:(Response.t -> unit) -> unit
+ (** Handlers are functions returning a response to a request.
+ The response can be delayed, hence the use of a continuation
+ as the [resp] parameter. *)
+
+ type t = handler -> handler
+ (** A middleware is a handler transformation.
+
+ It takes the existing handler [h],
+ and returns a new one which, given a query, modify it or log it
+ before passing it to [h], or fail. It can also log or modify or drop
+ the response. *)
+
+ val nil : t
+ (** Trivial middleware that does nothing. *)
+end
+
+(** {2 Main Server type} *)
+
+type t
+(** A HTTP server. See {!create} for more details. *)
+
+(** A backend that provides IO operations, network operations, etc.
+
+ This is used to decouple tiny_httpd from the scheduler/IO library used to
+ actually open a TCP server and talk to clients. The classic way is
+ based on {!Unix} and blocking IOs, but it's also possible to
+ use an OCaml 5 library using effects and non blocking IOs. *)
+module type IO_BACKEND = sig
+ val init_addr : unit -> string
+ (** Initial TCP address *)
+
+ val init_port : unit -> int
+ (** Initial port *)
+
+ val get_time_s : unit -> float
+ (** Obtain the current timestamp in seconds. *)
+
+ val tcp_server : unit -> IO.TCP_server.builder
+ (** TCP server builder, to create servers that can listen
+ on a port and handle clients. *)
+end
+
+val create_from :
+ ?buf_size:int ->
+ ?middlewares:([ `Encoding | `Stage of int ] * Middleware.t) list ->
+ backend:(module IO_BACKEND) ->
+ unit ->
+ t
+(** Create a new webserver using provided backend.
+
+ The server will not do anything until {!run} is called on it.
+ Before starting the server, one can use {!add_path_handler} and
+ {!set_top_handler} to specify how to handle incoming requests.
+
+ @param buf_size size for buffers (since 0.11)
+ @param middlewares see {!add_middleware} for more details.
+
+ @since 0.14
+*)
+
+val addr : t -> string
+(** Address on which the server listens. *)
+
+val is_ipv6 : t -> bool
+(** [is_ipv6 server] returns [true] iff the address of the server is an IPv6 address.
+ @since 0.3 *)
+
+val port : t -> int
+(** Port on which the server listens. Note that this might be different than
+ the port initially given if the port was [0] (meaning that the OS picks a
+ port for us). *)
+
+val active_connections : t -> int
+(** Number of currently active connections. *)
+
+val add_decode_request_cb :
+ t ->
+ (unit Request.t -> (unit Request.t * (IO.Input.t -> IO.Input.t)) option) ->
+ unit
+ [@@deprecated "use add_middleware"]
+(** Add a callback for every request.
+ The callback can provide a stream transformer and a new request (with
+ modified headers, typically).
+ A possible use is to handle decompression by looking for a [Transfer-Encoding]
+ header and returning a stream transformer that decompresses on the fly.
+
+ @deprecated use {!add_middleware} instead
+*)
+
+val add_encode_response_cb :
+ t -> (unit Request.t -> Response.t -> Response.t option) -> unit
+ [@@deprecated "use add_middleware"]
+(** Add a callback for every request/response pair.
+ Similarly to {!add_encode_response_cb} the callback can return a new
+ response, for example to compress it.
+ The callback is given the query with only its headers,
+ as well as the current response.
+
+ @deprecated use {!add_middleware} instead
+*)
+
+val add_middleware :
+ stage:[ `Encoding | `Stage of int ] -> t -> Middleware.t -> unit
+(** Add a middleware to every request/response pair.
+ @param stage specify when middleware applies.
+ Encoding comes first (outermost layer), then stages in increasing order.
+ @raise Invalid_argument if stage is [`Stage n] where [n < 1]
+ @since 0.11
+*)
+
+(** {2 Request handlers} *)
+
+val set_top_handler : t -> (IO.Input.t Request.t -> Response.t) -> unit
+(** Setup a handler called by default.
+
+ This handler is called with any request not accepted by any handler
+ installed via {!add_path_handler}.
+ If no top handler is installed, unhandled paths will return a [404] not found
+
+ This used to take a [string Request.t] but it now takes a [byte_stream Request.t]
+ since 0.14 . Use {!Request.read_body_full} to read the body into
+ a string if needed.
+*)
+
+val add_route_handler :
+ ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
+ ?middlewares:Middleware.t list ->
+ ?meth:Meth.t ->
+ t ->
+ ('a, string Request.t -> Response.t) Route.t ->
+ 'a ->
+ unit
+(** [add_route_handler server Route.(exact "path" @/ string @/ int @/ return) f]
+ calls [f "foo" 42 request] when a [request] with path "path/foo/42/"
+ is received.
+
+ Note that the handlers are called in the reverse order of their addition,
+ so the last registered handler can override previously registered ones.
+
+ @param meth if provided, only accept requests with the given method.
+ Typically one could react to [`GET] or [`PUT].
+ @param accept should return [Ok()] if the given request (before its body
+ is read) should be accepted, [Error (code,message)] if it's to be rejected (e.g. because
+ its content is too big, or for some permission error).
+ See the {!http_of_dir} program for an example of how to use [accept] to
+ filter uploads that are too large before the upload even starts.
+ The default always returns [Ok()], i.e. it accepts all requests.
+
+ @since 0.6
+*)
+
+val add_route_handler_stream :
+ ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
+ ?middlewares:Middleware.t list ->
+ ?meth:Meth.t ->
+ t ->
+ ('a, IO.Input.t Request.t -> Response.t) Route.t ->
+ 'a ->
+ unit
+(** Similar to {!add_route_handler}, but where the body of the request
+ is a stream of bytes that has not been read yet.
+ This is useful when one wants to stream the body directly into a parser,
+ json decoder (such as [Jsonm]) or into a file.
+ @since 0.6 *)
+
+(** {2 Server-sent events}
+
+ {b EXPERIMENTAL}: this API is not stable yet. *)
+
+(** A server-side function to generate of Server-sent events.
+
+ See {{: https://html.spec.whatwg.org/multipage/server-sent-events.html} the w3c page}
+ and {{: https://jvns.ca/blog/2021/01/12/day-36--server-sent-events-are-cool--and-a-fun-bug/}
+ this blog post}.
+
+ @since 0.9
+ *)
+module type SERVER_SENT_GENERATOR = sig
+ val set_headers : Headers.t -> unit
+ (** Set headers of the response.
+ This is not mandatory but if used at all, it must be called before
+ any call to {!send_event} (once events are sent the response is
+ already sent too). *)
+
+ val send_event :
+ ?event:string -> ?id:string -> ?retry:string -> data:string -> unit -> unit
+ (** Send an event from the server.
+ If data is a multiline string, it will be sent on separate "data:" lines. *)
+
+ val close : unit -> unit
+ (** Close connection.
+ @since 0.11 *)
+end
+
+type server_sent_generator = (module SERVER_SENT_GENERATOR)
+(** Server-sent event generator. This generates events that are forwarded to
+ the client (e.g. the browser).
+ @since 0.9 *)
+
+val add_route_server_sent_handler :
+ ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
+ t ->
+ ('a, string Request.t -> server_sent_generator -> unit) Route.t ->
+ 'a ->
+ unit
+(** Add a handler on an endpoint, that serves server-sent events.
+
+ The callback is given a generator that can be used to send events
+ as it pleases. The connection is always closed by the client,
+ and the accepted method is always [GET].
+ This will set the header "content-type" to "text/event-stream" automatically
+ and reply with a 200 immediately.
+ See {!server_sent_generator} for more details.
+
+ This handler stays on the original thread (it is synchronous).
+
+ @since 0.9 *)
+
+(** {2 Upgrade handlers}
+
+ These handlers upgrade the connection to another protocol.
+ @since NEXT_RELEASE *)
+
+(** Handler that upgrades to another protocol.
+ @since NEXT_RELEASE *)
+module type UPGRADE_HANDLER = sig
+ type handshake_state
+ (** Some specific state returned after handshake *)
+
+ val name : string
+ (** Name in the "upgrade" header *)
+
+ val handshake : unit Request.t -> (Headers.t * handshake_state, string) result
+ (** Perform the handshake and upgrade the connection. The returned
+ code is [101] alongside these headers.
+ In case the handshake fails, this only returns [Error log_msg].
+ The connection is closed without further ado. *)
+
+ val handle_connection :
+ Unix.sockaddr -> handshake_state -> IO.Input.t -> IO.Output.t -> unit
+ (** Take control of the connection and take it from ther.e *)
+end
+
+type upgrade_handler = (module UPGRADE_HANDLER)
+(** @since NEXT_RELEASE *)
+
+val add_upgrade_handler :
+ ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
+ t ->
+ ('a, upgrade_handler) Route.t ->
+ 'a ->
+ unit
+
+(** {2 Run the server} *)
+
+val running : t -> bool
+(** Is the server running?
+ @since 0.14 *)
+
+val stop : t -> unit
+(** Ask the server to stop. This might not have an immediate effect
+ as {!run} might currently be waiting on IO. *)
+
+val run : ?after_init:(unit -> unit) -> t -> (unit, exn) result
+(** Run the main loop of the server, listening on a socket
+ described at the server's creation time, using [new_thread] to
+ start a thread for each new client.
+
+ This returns [Ok ()] if the server exits gracefully, or [Error e] if
+ it exits with an error.
+
+ @param after_init is called after the server starts listening. since 0.13 .
+*)
+
+val run_exn : ?after_init:(unit -> unit) -> t -> unit
+(** [run_exn s] is like [run s] but re-raises an exception if the server exits
+ with an error.
+ @since 0.14 *)
diff --git a/src/Tiny_httpd_stream.ml b/src/core/stream.ml.tmp
similarity index 89%
rename from src/Tiny_httpd_stream.ml
rename to src/core/stream.ml.tmp
index a845c8bf..607a4f5f 100644
--- a/src/Tiny_httpd_stream.ml
+++ b/src/core/stream.ml.tmp
@@ -1,3 +1,4 @@
+(*
module Buf = Tiny_httpd_buf
module IO = Tiny_httpd_io
@@ -184,36 +185,6 @@ let read_line_into (self : t) ~buf : unit =
)
done
-(* new stream with maximum size [max_size].
- @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 *)
-let limit_size_to ~close_rec ~max_size ~too_big (arg : t) : t =
- let size = ref 0 in
- let continue = ref true in
- make ~bs:Bytes.empty
- ~close:(fun _ -> if close_rec then arg.close ())
- ~fill:(fun res ->
- if res.len = 0 && !continue then (
- arg.fill_buf ();
- res.bs <- arg.bs;
- res.off <- arg.off;
- res.len <- arg.len
- ) else (
- arg.bs <- Bytes.empty;
- arg.off <- 0;
- arg.len <- 0
- ))
- ~consume:(fun res n ->
- size := !size + n;
- if !size > max_size then (
- continue := false;
- too_big !size
- ) else (
- arg.consume n;
- res.off <- res.off + n;
- res.len <- res.len - n
- ))
- ()
(* read exactly [size] bytes from the stream *)
let read_exactly ~close_rec ~size ~too_short (arg : t) : t =
@@ -320,3 +291,4 @@ let output_chunked' ?buf (oc : IO.Output.t) (self : t) : unit =
(* print a stream as a series of chunks *)
let output_chunked ?buf (oc : out_channel) (self : t) : unit =
output_chunked' ?buf (IO.Output.of_out_channel oc) self
+ *)
diff --git a/src/Tiny_httpd_stream.mli b/src/core/stream.mli.tmp
similarity index 98%
rename from src/Tiny_httpd_stream.mli
rename to src/core/stream.mli.tmp
index a5b5636d..5d2facad 100644
--- a/src/Tiny_httpd_stream.mli
+++ b/src/core/stream.mli.tmp
@@ -64,7 +64,7 @@ val close : t -> unit
val empty : t
(** Stream with 0 bytes inside *)
-val of_input : ?buf_size:int -> Tiny_httpd_io.Input.t -> t
+val of_input : ?buf_size:int -> Io.Input.t -> t
(** Make a buffered stream from the given channel.
@since 0.14 *)
diff --git a/src/Tiny_httpd_util.ml b/src/core/util.ml
similarity index 94%
rename from src/Tiny_httpd_util.ml
rename to src/core/util.ml
index 73617702..38d9bbb9 100644
--- a/src/Tiny_httpd_util.ml
+++ b/src/core/util.ml
@@ -76,6 +76,12 @@ let split_on_slash s : _ list =
List.rev !l
let parse_query s : (_ list, string) result =
+ let s =
+ (* skip hash if present *)
+ match String.index_opt s '#' with
+ | Some i -> String.sub s (i + 1) (String.length s - i - 1)
+ | None -> s
+ in
let pairs = ref [] in
let is_sep_ = function
| '&' | ';' -> true
@@ -119,3 +125,5 @@ let show_sockaddr = function
| Unix.ADDR_UNIX f -> f
| Unix.ADDR_INET (inet, port) ->
Printf.sprintf "%s:%d" (Unix.string_of_inet_addr inet) port
+
+let is_ipv6_str addr : bool = String.contains addr ':'
diff --git a/src/Tiny_httpd_util.mli b/src/core/util.mli
similarity index 91%
rename from src/Tiny_httpd_util.mli
rename to src/core/util.mli
index ac996855..1e5a20f3 100644
--- a/src/Tiny_httpd_util.mli
+++ b/src/core/util.mli
@@ -38,3 +38,7 @@ val parse_query : string -> ((string * string) list, string) result
val show_sockaddr : Unix.sockaddr -> string
(** Simple printer for socket addresses.
@since NEXT_RELEASE *)
+
+val is_ipv6_str : string -> bool
+(** Is this string potentially an IPV6 address?
+ @since NEXT_RELEASE *)
diff --git a/src/dune b/src/dune
index 74542c39..81f895bd 100644
--- a/src/dune
+++ b/src/dune
@@ -1,30 +1,6 @@
(library
(name tiny_httpd)
(public_name tiny_httpd)
- (private_modules Tiny_httpd_mime_ Tiny_httpd_parse_)
- (libraries threads seq unix
- (select Tiny_httpd_mime_.ml from
- (magic-mime -> Tiny_httpd_mime_.magic.ml)
- ( -> Tiny_httpd_mime_.dummy.ml))
- (select Tiny_httpd_log.ml from
- (logs logs.fmt fmt.tty -> Tiny_httpd_log.logs.ml)
- (-> Tiny_httpd_log.default.ml)))
- (wrapped false))
-
-(rule
- (targets Tiny_httpd_html_.ml)
- (deps
- (:bin ./gen/gentags.exe))
- (action
- (with-stdout-to
- %{targets}
- (run %{bin}))))
-
-(rule
- (targets Tiny_httpd_atomic_.ml)
- (deps
- (:bin ./gen/mkshims.exe))
- (action
- (with-stdout-to
- %{targets}
- (run %{bin}))))
+ (flags :standard -open Tiny_httpd_core)
+ (libraries threads seq unix hmap tiny_httpd.core tiny_httpd.html
+ tiny_httpd.unix))
diff --git a/src/gen/dune b/src/gen/dune
deleted file mode 100644
index 6cd2fd4a..00000000
--- a/src/gen/dune
+++ /dev/null
@@ -1,2 +0,0 @@
-(executables
- (names gentags mkshims))
diff --git a/src/Tiny_httpd_html.ml b/src/html/Tiny_httpd_html.ml
similarity index 87%
rename from src/Tiny_httpd_html.ml
rename to src/html/Tiny_httpd_html.ml
index 61f0416b..af9cf45d 100644
--- a/src/Tiny_httpd_html.ml
+++ b/src/html/Tiny_httpd_html.ml
@@ -6,9 +6,7 @@
@since 0.12
*)
-module IO = Tiny_httpd_io
-
-include Tiny_httpd_html_
+include Html_
(** @inline *)
(** Write an HTML element to this output.
@@ -16,7 +14,7 @@ include Tiny_httpd_html_
be a "html" tag.
@since 0.14
*)
-let to_output ?(top = false) (self : elt) (out : IO.Output.t) : unit =
+let to_output ?(top = false) (self : elt) (out : #IO.Output.t) : unit =
let out = Out.create_of_out out in
if top then Out.add_string out "\n";
self out;
@@ -56,10 +54,10 @@ let to_out_channel_top = to_output ~top:true
@param top if true, add a DOCTYPE. See {!to_out_channel}.
@since 0.14 *)
let to_writer ?top (self : elt) : IO.Writer.t =
- let write oc = to_output ?top self oc in
+ let write (oc : #IO.Output.t) = to_output ?top self oc in
IO.Writer.make ~write ()
(** Convert a HTML element to a stream. This might just convert
it to a string first, do not assume it to be more efficient. *)
-let to_stream (self : elt) : Tiny_httpd_stream.t =
- Tiny_httpd_stream.of_string @@ to_string self
+let[@inline] to_stream (self : elt) : IO.Input.t =
+ IO.Input.of_string @@ to_string self
diff --git a/src/html/dune b/src/html/dune
new file mode 100644
index 00000000..64386122
--- /dev/null
+++ b/src/html/dune
@@ -0,0 +1,16 @@
+
+
+(library
+ (name tiny_httpd_html)
+ (public_name tiny_httpd.html)
+ (flags :standard -open Tiny_httpd_core)
+ (libraries seq tiny_httpd.core))
+
+(rule
+ (targets html_.ml)
+ (deps
+ (:bin ./gen/gentags.exe))
+ (action
+ (with-stdout-to
+ %{targets}
+ (run %{bin}))))
diff --git a/src/html/gen/dune b/src/html/gen/dune
new file mode 100644
index 00000000..3acc658b
--- /dev/null
+++ b/src/html/gen/dune
@@ -0,0 +1,2 @@
+(executables
+ (names gentags))
diff --git a/src/gen/gentags.ml b/src/html/gen/gentags.ml
similarity index 99%
rename from src/gen/gentags.ml
rename to src/html/gen/gentags.ml
index c23bcfbc..2736b796 100644
--- a/src/gen/gentags.ml
+++ b/src/html/gen/gentags.ml
@@ -294,14 +294,13 @@ let prelude =
module Out : sig
type t
val create_of_buffer : Buffer.t -> t
- val create_of_out: Tiny_httpd_io.Output.t -> t
+ val create_of_out: IO.Output.t -> t
val flush : t -> unit
val add_char : t -> char -> unit
val add_string : t -> string -> unit
val add_format_nl : t -> unit
val with_no_format_nl : t -> (unit -> 'a) -> 'a
end = struct
- module IO = Tiny_httpd_io
type t = {
out: IO.Output.t;
mutable fmt_nl: bool; (* if true, we print [\n] around tags to format the html *)
diff --git a/src/prometheus/common_.ml b/src/prometheus/common_.ml
deleted file mode 100644
index bb70b2d7..00000000
--- a/src/prometheus/common_.ml
+++ /dev/null
@@ -1,3 +0,0 @@
-module A = Tiny_httpd_atomic_
-
-let spf = Printf.sprintf
diff --git a/src/prometheus/common_p_.ml b/src/prometheus/common_p_.ml
new file mode 100644
index 00000000..812670ab
--- /dev/null
+++ b/src/prometheus/common_p_.ml
@@ -0,0 +1,3 @@
+module A = Tiny_httpd_core.Atomic_
+
+let spf = Printf.sprintf
diff --git a/src/prometheus/dune b/src/prometheus/dune
index 5da724e5..3439a474 100644
--- a/src/prometheus/dune
+++ b/src/prometheus/dune
@@ -4,9 +4,10 @@
(name tiny_httpd_prometheus)
(public_name tiny_httpd.prometheus)
(synopsis "Metrics using prometheus")
- (private_modules common_ time_)
+ (private_modules common_p_ time_)
+ (flags :standard -open Tiny_httpd_core)
(libraries
- tiny_httpd unix
+ tiny_httpd.core unix
(select time_.ml from
(mtime mtime.clock.os -> time_.mtime.ml)
(-> time_.default.ml))))
diff --git a/src/prometheus/tiny_httpd_prometheus.ml b/src/prometheus/tiny_httpd_prometheus.ml
index 325018a8..b3ec4e39 100644
--- a/src/prometheus/tiny_httpd_prometheus.ml
+++ b/src/prometheus/tiny_httpd_prometheus.ml
@@ -2,7 +2,7 @@
https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
*)
-open Common_
+open Common_p_
let bpf = Printf.bprintf
@@ -175,9 +175,7 @@ end
let global = Registry.create ()
-module H = Tiny_httpd
-
-let http_middleware (reg : Registry.t) : H.Middleware.t =
+let http_middleware (reg : Registry.t) : Server.Middleware.t =
let c_req =
Counter.create reg "tiny_httpd_requests" ~descr:"number of HTTP requests"
in
@@ -189,11 +187,11 @@ let http_middleware (reg : Registry.t) : H.Middleware.t =
~buckets:[ 0.001; 0.01; 0.1; 0.5; 1.; 5.; 10. ]
in
- fun h : H.Middleware.handler ->
+ fun h : Server.Middleware.handler ->
fun req ~resp : unit ->
let start = Time_.now_us () in
Counter.incr c_req;
- h req ~resp:(fun (response : H.Response.t) ->
+ h req ~resp:(fun (response : Response.t) ->
let code = response.code in
let elapsed_us = Time_.now_us () -. start in
@@ -203,13 +201,14 @@ let http_middleware (reg : Registry.t) : H.Middleware.t =
if code < 200 || code >= 400 then Counter.incr c_err;
resp response)
-let add_route_to_server (server : H.t) (reg : registry) : unit =
- H.add_route_handler server H.Route.(exact "metrics" @/ return) @@ fun _req ->
+let add_route_to_server (server : Server.t) (reg : registry) : unit =
+ Server.add_route_handler server Route.(exact "metrics" @/ return)
+ @@ fun _req ->
let str = Registry.emit_str reg in
- H.Response.make_string @@ Ok str
+ Response.make_string @@ Ok str
-let instrument_server (server : H.t) reg : unit =
- H.add_middleware ~stage:(`Stage 1) server (http_middleware reg);
+let instrument_server (server : Server.t) reg : unit =
+ Server.add_middleware ~stage:(`Stage 1) server (http_middleware reg);
add_route_to_server server reg
module GC_metrics = struct
diff --git a/src/prometheus/tiny_httpd_prometheus.mli b/src/prometheus/tiny_httpd_prometheus.mli
index b634943d..dfac868a 100644
--- a/src/prometheus/tiny_httpd_prometheus.mli
+++ b/src/prometheus/tiny_httpd_prometheus.mli
@@ -77,13 +77,13 @@ end
end
*)
-val http_middleware : Registry.t -> Tiny_httpd.Middleware.t
+val http_middleware : Registry.t -> Server.Middleware.t
(** Middleware to get basic metrics about HTTP requests *)
-val add_route_to_server : Tiny_httpd.t -> Registry.t -> unit
+val add_route_to_server : Server.t -> Registry.t -> unit
(** Add a "/metrics" route to the server *)
-val instrument_server : Tiny_httpd.t -> Registry.t -> unit
+val instrument_server : Server.t -> Registry.t -> unit
(** Add middleware and route *)
module GC_metrics : sig
diff --git a/src/Tiny_httpd_dir.ml b/src/unix/dir.ml
similarity index 86%
rename from src/Tiny_httpd_dir.ml
rename to src/unix/dir.ml
index c619c217..0035849c 100644
--- a/src/Tiny_httpd_dir.ml
+++ b/src/unix/dir.ml
@@ -1,7 +1,7 @@
-module S = Tiny_httpd_server
-module U = Tiny_httpd_util
+module S = Server
+module U = Util
module Html = Tiny_httpd_html
-module Log = Tiny_httpd_log
+module Log = Log
type dir_behavior = Index | Lists | Index_or_lists | Forbidden
type hidden = unit
@@ -78,7 +78,7 @@ module type VFS = sig
val list_dir : string -> string array
val delete : string -> unit
val create : string -> (bytes -> int -> int -> unit) * (unit -> unit)
- val read_file_content : string -> Tiny_httpd_stream.t
+ val read_file_content : string -> IO.Input.t
val file_size : string -> int option
val file_mtime : string -> float option
end
@@ -99,7 +99,8 @@ let vfs_of_dir (top : string) : vfs =
| { st_kind = Unix.S_REG; _ } ->
let ic = Unix.(openfile fpath [ O_RDONLY ] 0) in
let closed = ref false in
- Tiny_httpd_stream.of_fd_close_noerr ~closed ic
+ let buf = IO.Slice.create 4096 in
+ IO.Input.of_unix_fd ~buf ~close_noerr:true ~closed ic
| _ -> failwith (Printf.sprintf "not a regular file: %S" f)
let create f =
@@ -216,51 +217,52 @@ let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
: unit =
let route () =
if prefix = "" then
- S.Route.rest_of_path_urlencoded
+ Route.rest_of_path_urlencoded
else
- S.Route.exact_path prefix S.Route.rest_of_path_urlencoded
+ Route.exact_path prefix Route.rest_of_path_urlencoded
in
if config.delete then
S.add_route_handler server ~meth:`DELETE (route ()) (fun path _req ->
if contains_dot_dot path then
- S.Response.fail_raise ~code:403 "invalid path in delete"
+ Response.fail_raise ~code:403 "invalid path in delete"
else
- S.Response.make_string
+ Response.make_string
(try
VFS.delete path;
Ok "file deleted successfully"
with e -> Error (500, Printexc.to_string e)))
else
S.add_route_handler server ~meth:`DELETE (route ()) (fun _ _ ->
- S.Response.make_raw ~code:405 "delete not allowed");
+ Response.make_raw ~code:405 "delete not allowed");
if config.upload then
S.add_route_handler_stream server ~meth:`PUT (route ())
~accept:(fun req ->
- match S.Request.get_header_int req "Content-Length" with
+ match Request.get_header_int req "Content-Length" with
| Some n when n > 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.Request.path ->
Error (403, "invalid path (contains '..')")
| _ -> Ok ())
(fun path req ->
let write, close =
try VFS.create path
with e ->
- S.Response.fail_raise ~code:403 "cannot upload to %S: %s" path
+ Response.fail_raise ~code:403 "cannot upload to %S: %s" path
(Printexc.to_string e)
in
let req =
- S.Request.limit_body_size ~max_size:config.max_upload_size req
+ Request.limit_body_size ~bytes:(Bytes.create 4096)
+ ~max_size:config.max_upload_size req
in
- Tiny_httpd_stream.iter write req.S.Request.body;
+ IO.Input.iter write req.body;
close ();
Log.debug (fun k -> k "dir: done uploading file to %S" path);
- S.Response.make_raw ~code:201 "upload successful")
+ Response.make_raw ~code:201 "upload successful")
else
S.add_route_handler server ~meth:`PUT (route ()) (fun _ _ ->
- S.Response.make_raw ~code:405 "upload not allowed");
+ Response.make_raw ~code:405 "upload not allowed");
if config.download then
S.add_route_handler server ~meth:`GET (route ()) (fun path req ->
@@ -268,19 +270,18 @@ let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
let mtime =
lazy
(match VFS.file_mtime path with
- | None -> S.Response.fail_raise ~code:403 "Cannot access file"
+ | None -> Response.fail_raise ~code:403 "Cannot access file"
| Some t -> Printf.sprintf "mtime: %.4f" t)
in
if contains_dot_dot path then
- S.Response.fail ~code:403 "Path is forbidden"
+ Response.fail ~code:403 "Path is forbidden"
else if not (VFS.contains path) then
- S.Response.fail ~code:404 "File not found"
- else if
- S.Request.get_header req "If-None-Match" = Some (Lazy.force mtime)
+ Response.fail ~code:404 "File not found"
+ else if Request.get_header req "If-None-Match" = Some (Lazy.force mtime)
then (
Log.debug (fun k ->
k "dir: cached object %S (etag: %S)" path (Lazy.force mtime));
- S.Response.make_raw ~code:304 ""
+ Response.make_raw ~code:304 ""
) else if VFS.is_directory path then (
Log.debug (fun k -> k "dir: list dir %S (topdir %S)" path VFS.descr);
let parent = Filename.(dirname path) in
@@ -295,33 +296,36 @@ let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
(* redirect using path, not full path *)
let new_path = "/" // prefix // path // "index.html" in
Log.debug (fun k -> k "dir: redirect to `%s`" new_path);
- S.Response.make_void ~code:301 ()
- ~headers:S.Headers.(empty |> set "location" new_path)
+ Response.make_void ~code:301 ()
+ ~headers:Headers.(empty |> set "location" new_path)
| Lists | Index_or_lists ->
let body =
html_list_dir ~prefix vfs path ~parent |> Html.to_string_top
in
- S.Response.make_string
+ Response.make_string
~headers:[ header_html; "ETag", Lazy.force mtime ]
(Ok body)
| Forbidden | Index ->
- S.Response.make_raw ~code:405 "listing dir not allowed"
+ Response.make_raw ~code:405 "listing dir not allowed"
) else (
try
let mime_type =
- if Filename.extension path = ".css" then
+ (* FIXME: handle .html specially *)
+ if Filename.extension path = ".html" then
+ [ "Content-Type", "text/html" ]
+ else if Filename.extension path = ".css" then
[ "Content-Type", "text/css" ]
else if Filename.extension path = ".js" then
[ "Content-Type", "text/javascript" ]
else if on_fs then (
(* call "file" util *)
- let ty = Tiny_httpd_mime_.mime_of_path (top // path) in
+ let ty = Mime_.mime_of_path (top // path) in
[ "content-type", ty ]
) else
[]
in
let stream = VFS.read_file_content path in
- S.Response.make_raw_stream
+ Response.make_raw_stream
~headers:(mime_type @ [ "Etag", Lazy.force mtime ])
~code:200 stream
with e ->
@@ -330,11 +334,11 @@ let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
Log.error (fun k ->
k "dir.get failed: %s@.%s" msg
(Printexc.raw_backtrace_to_string bt));
- S.Response.fail ~code:500 "error while reading file: %s" msg
+ Response.fail ~code:500 "error while reading file: %s" msg
))
else
S.add_route_handler server ~meth:`GET (route ()) (fun _ _ ->
- S.Response.make_raw ~code:405 "download not allowed");
+ Response.make_raw ~code:405 "download not allowed");
()
let add_vfs ~config ~vfs ~prefix server : unit =
@@ -437,7 +441,7 @@ module Embedded_fs = struct
let read_file_content p =
match find_ self p with
- | Some (File { content; _ }) -> Tiny_httpd_stream.of_string content
+ | Some (File { content; _ }) -> IO.Input.of_string content
| _ -> failwith (Printf.sprintf "no such file: %S" p)
let list_dir p =
diff --git a/src/Tiny_httpd_dir.mli b/src/unix/dir.mli
similarity index 94%
rename from src/Tiny_httpd_dir.mli
rename to src/unix/dir.mli
index 9590bd60..b07029f9 100644
--- a/src/Tiny_httpd_dir.mli
+++ b/src/unix/dir.mli
@@ -60,7 +60,7 @@ val config :
@since 0.12 *)
val add_dir_path :
- config:config -> dir:string -> prefix:string -> Tiny_httpd_server.t -> unit
+ config:config -> dir:string -> prefix:string -> Server.t -> unit
(** [add_dirpath ~config ~dir ~prefix server] adds route handle to the
[server] to serve static files in [dir] when url starts with [prefix],
using the given configuration [config]. *)
@@ -91,7 +91,7 @@ module type VFS = sig
val create : string -> (bytes -> int -> int -> unit) * (unit -> unit)
(** Create a file and obtain a pair [write, close] *)
- val read_file_content : string -> Tiny_httpd_stream.t
+ val read_file_content : string -> IO.Input.t
(** Read content of a file *)
val file_size : string -> int option
@@ -108,11 +108,7 @@ val vfs_of_dir : string -> (module VFS)
*)
val add_vfs :
- config:config ->
- vfs:(module VFS) ->
- prefix:string ->
- Tiny_httpd_server.t ->
- unit
+ config:config -> vfs:(module VFS) -> prefix:string -> Server.t -> unit
(** Similar to {!add_dir_path} but using a virtual file system instead.
@since 0.12
*)
diff --git a/src/unix/dune b/src/unix/dune
new file mode 100644
index 00000000..b10ee59e
--- /dev/null
+++ b/src/unix/dune
@@ -0,0 +1,12 @@
+
+(library
+ (name tiny_httpd_unix)
+ (public_name tiny_httpd.unix)
+ (synopsis "Backend based on Unix and blocking IOs for Tiny_httpd")
+ (flags :standard -open Tiny_httpd_core)
+ (private_modules mime_)
+ (libraries tiny_httpd.core tiny_httpd.html unix
+ (select mime_.ml from
+ (magic-mime -> mime_.magic.ml)
+ ( -> mime_.dummy.ml))
+ ))
diff --git a/src/Tiny_httpd_mime_.dummy.ml b/src/unix/mime_.dummy.ml
similarity index 100%
rename from src/Tiny_httpd_mime_.dummy.ml
rename to src/unix/mime_.dummy.ml
diff --git a/src/Tiny_httpd_mime_.magic.ml b/src/unix/mime_.magic.ml
similarity index 100%
rename from src/Tiny_httpd_mime_.magic.ml
rename to src/unix/mime_.magic.ml
diff --git a/src/Tiny_httpd_mime_.mli b/src/unix/mime_.mli
similarity index 100%
rename from src/Tiny_httpd_mime_.mli
rename to src/unix/mime_.mli
diff --git a/src/unix/sem.ml b/src/unix/sem.ml
new file mode 100644
index 00000000..83159589
--- /dev/null
+++ b/src/unix/sem.ml
@@ -0,0 +1,25 @@
+(** semaphore, for limiting concurrency. *)
+
+type t = { mutable n: int; max: int; mutex: Mutex.t; cond: Condition.t }
+
+let create n =
+ if n <= 0 then invalid_arg "Semaphore.create";
+ { n; max = n; mutex = Mutex.create (); cond = Condition.create () }
+
+let acquire m t =
+ Mutex.lock t.mutex;
+ while t.n < m do
+ Condition.wait t.cond t.mutex
+ done;
+ assert (t.n >= m);
+ t.n <- t.n - m;
+ Condition.broadcast t.cond;
+ Mutex.unlock t.mutex
+
+let release m t =
+ Mutex.lock t.mutex;
+ t.n <- t.n + m;
+ Condition.broadcast t.cond;
+ Mutex.unlock t.mutex
+
+let num_acquired t = t.max - t.n
diff --git a/src/unix/tiny_httpd_unix.ml b/src/unix/tiny_httpd_unix.ml
new file mode 100644
index 00000000..f1de3936
--- /dev/null
+++ b/src/unix/tiny_httpd_unix.ml
@@ -0,0 +1,155 @@
+module Dir = Dir
+module Sem = Sem
+
+module Unix_tcp_server_ = struct
+ let get_addr_ sock =
+ match Unix.getsockname sock with
+ | Unix.ADDR_INET (addr, port) -> addr, port
+ | _ -> invalid_arg "httpd: address is not INET"
+
+ type t = {
+ addr: string;
+ port: int;
+ buf_pool: Buf.t Pool.t;
+ slice_pool: IO.Slice.t Pool.t;
+ max_connections: int;
+ sem_max_connections: Sem.t;
+ (** semaphore to restrict the number of active concurrent connections *)
+ mutable sock: Unix.file_descr option; (** Socket *)
+ new_thread: (unit -> unit) -> unit;
+ timeout: float;
+ masksigpipe: bool;
+ mutable running: bool; (* TODO: use an atomic? *)
+ }
+
+ let shutdown_silent_ fd =
+ try Unix.shutdown fd Unix.SHUTDOWN_ALL with _ -> ()
+
+ let close_silent_ fd = try Unix.close fd with _ -> ()
+
+ let to_tcp_server (self : t) : IO.TCP_server.builder =
+ {
+ IO.TCP_server.serve =
+ (fun ~after_init ~handle () : unit ->
+ if self.masksigpipe then
+ ignore (Unix.sigprocmask Unix.SIG_BLOCK [ Sys.sigpipe ] : _ list);
+ let sock, should_bind =
+ match self.sock with
+ | Some s ->
+ ( s,
+ false
+ (* Because we're getting a socket from the caller (e.g. systemd) *)
+ )
+ | None ->
+ ( Unix.socket
+ (if Util.is_ipv6_str self.addr then
+ Unix.PF_INET6
+ else
+ Unix.PF_INET)
+ Unix.SOCK_STREAM 0,
+ true (* Because we're creating the socket ourselves *) )
+ in
+ Unix.clear_nonblock sock;
+ Unix.setsockopt_optint sock Unix.SO_LINGER None;
+ if should_bind then (
+ let inet_addr = Unix.inet_addr_of_string self.addr in
+ Unix.setsockopt sock Unix.SO_REUSEADDR true;
+ Unix.bind sock (Unix.ADDR_INET (inet_addr, self.port));
+ let n_listen = 2 * self.max_connections in
+ Unix.listen sock n_listen
+ );
+
+ self.sock <- Some sock;
+
+ let tcp_server =
+ {
+ IO.TCP_server.stop = (fun () -> self.running <- false);
+ running = (fun () -> self.running);
+ active_connections =
+ (fun () -> Sem.num_acquired self.sem_max_connections - 1);
+ endpoint =
+ (fun () ->
+ let addr, port = get_addr_ sock in
+ Unix.string_of_inet_addr addr, port);
+ }
+ in
+ after_init tcp_server;
+
+ (* how to handle a single client *)
+ let handle_client_unix_ (client_sock : Unix.file_descr)
+ (client_addr : Unix.sockaddr) : unit =
+ Log.debug (fun k ->
+ k "t[%d]: serving new client on %s"
+ (Thread.id @@ Thread.self ())
+ (Util.show_sockaddr client_addr));
+
+ if self.masksigpipe then
+ ignore (Unix.sigprocmask Unix.SIG_BLOCK [ Sys.sigpipe ] : _ list);
+ Unix.set_nonblock client_sock;
+ Unix.setsockopt client_sock Unix.TCP_NODELAY true;
+ Unix.(setsockopt_float client_sock SO_RCVTIMEO self.timeout);
+ Unix.(setsockopt_float client_sock SO_SNDTIMEO self.timeout);
+
+ Pool.with_resource self.slice_pool @@ fun ic_buf ->
+ Pool.with_resource self.slice_pool @@ fun oc_buf ->
+ let closed = ref false in
+
+ let oc =
+ new IO.Output.of_unix_fd
+ ~close_noerr:true ~closed ~buf:oc_buf client_sock
+ in
+ let ic =
+ IO.Input.of_unix_fd ~close_noerr:true ~closed ~buf:ic_buf
+ client_sock
+ in
+ handle.handle ~client_addr ic oc
+ in
+
+ Unix.set_nonblock sock;
+ while self.running do
+ match Unix.accept sock with
+ | client_sock, client_addr ->
+ (* limit concurrency *)
+ Sem.acquire 1 self.sem_max_connections;
+ (* Block INT/HUP while cloning to avoid children handling them.
+ When thread gets them, our Unix.accept raises neatly. *)
+ ignore Unix.(sigprocmask SIG_BLOCK Sys.[ sigint; sighup ]);
+ self.new_thread (fun () ->
+ try
+ handle_client_unix_ client_sock client_addr;
+ Log.debug (fun k ->
+ k "t[%d]: done with client on %s, exiting"
+ (Thread.id @@ Thread.self ())
+ @@ Util.show_sockaddr client_addr);
+ shutdown_silent_ client_sock;
+ close_silent_ client_sock;
+ Sem.release 1 self.sem_max_connections
+ with e ->
+ let bt = Printexc.get_raw_backtrace () in
+ shutdown_silent_ client_sock;
+ close_silent_ client_sock;
+ Sem.release 1 self.sem_max_connections;
+ Log.error (fun k ->
+ k
+ "@[Handler: uncaught exception for client %s:@ \
+ %s@ %s@]"
+ (Util.show_sockaddr client_addr)
+ (Printexc.to_string e)
+ (Printexc.raw_backtrace_to_string bt)));
+ ignore Unix.(sigprocmask SIG_UNBLOCK Sys.[ sigint; sighup ])
+ | exception Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK), _, _)
+ ->
+ (* wait for the socket to be ready, and re-enter the loop *)
+ ignore (Unix.select [ sock ] [] [ sock ] 1.0 : _ * _ * _)
+ | exception e ->
+ Log.error (fun k ->
+ k "Unix.accept raised an exception: %s" (Printexc.to_string e));
+ Thread.delay 0.01
+ done;
+
+ (* Wait for all threads to be done: this only works if all threads are done. *)
+ Unix.close sock;
+ Sem.acquire self.sem_max_connections.max self.sem_max_connections;
+ ());
+ }
+end
diff --git a/src/ws/common_.ml b/src/ws/common_ws_.ml
similarity index 100%
rename from src/ws/common_.ml
rename to src/ws/common_ws_.ml
diff --git a/src/ws/dune b/src/ws/dune
index f2aab877..307cd559 100644
--- a/src/ws/dune
+++ b/src/ws/dune
@@ -3,9 +3,10 @@
(name tiny_httpd_ws)
(public_name tiny_httpd.ws)
(synopsis "Websockets for tiny_httpd")
- (private_modules common_ utils_)
+ (private_modules common_ws_ utils_)
+ (flags :standard -open Tiny_httpd_core)
(foreign_stubs
(language c)
(names tiny_httpd_ws_stubs)
(flags :standard -std=c99 -fPIC -O2))
- (libraries tiny_httpd threads))
+ (libraries tiny_httpd.core threads))
diff --git a/src/ws/tiny_httpd_ws.ml b/src/ws/tiny_httpd_ws.ml
index 80867d95..3917ef0e 100644
--- a/src/ws/tiny_httpd_ws.ml
+++ b/src/ws/tiny_httpd_ws.ml
@@ -1,7 +1,4 @@
-open Common_
-open Tiny_httpd_server
-module Log = Tiny_httpd_log
-module IO = Tiny_httpd_io
+open Common_ws_
type handler = Unix.sockaddr -> IO.Input.t -> IO.Output.t -> unit
@@ -382,21 +379,23 @@ let upgrade ic oc : _ * _ =
let writer = Writer.create ~oc () in
let reader = Reader.create ~ic ~writer () in
let ws_ic : IO.Input.t =
- {
- input = (fun buf i len -> Reader.read reader buf i len);
- close = (fun () -> Reader.close reader);
- }
+ object
+ inherit IO.Input.t_from_refill ~bytes:(Bytes.create 4_096) ()
+
+ method private refill (slice : IO.Slice.t) =
+ slice.off <- 0;
+ slice.len <- Reader.read reader slice.bytes 0 (Bytes.length slice.bytes)
+
+ method close () = Reader.close reader
+ end
in
let ws_oc : IO.Output.t =
- {
- flush =
- (fun () ->
- Writer.flush writer;
- IO.Output.flush oc);
- output_char = Writer.output_char writer;
- output = Writer.output writer;
- close = (fun () -> Writer.close writer);
- }
+ object
+ method close () = Writer.close writer
+ method flush () = Writer.flush writer
+ method output bs i len = Writer.output writer bs i len
+ method output_char c = Writer.output_char writer c
+ end
in
ws_ic, ws_oc
@@ -404,7 +403,7 @@ let upgrade ic oc : _ * _ =
module Make_upgrade_handler (X : sig
val accept_ws_protocol : string -> bool
val handler : handler
-end) : UPGRADE_HANDLER = struct
+end) : Server.UPGRADE_HANDLER = struct
type handshake_state = unit
let name = "websocket"
@@ -454,10 +453,10 @@ end) : UPGRADE_HANDLER = struct
end
let add_route_handler ?accept ?(accept_ws_protocol = fun _ -> true)
- (server : Tiny_httpd_server.t) route (f : handler) : unit =
+ (server : Server.t) route (f : handler) : unit =
let module M = Make_upgrade_handler (struct
let handler = f
let accept_ws_protocol = accept_ws_protocol
end) in
- let up : upgrade_handler = (module M) in
- Tiny_httpd_server.add_upgrade_handler ?accept server route up
+ let up : Server.upgrade_handler = (module M) in
+ Server.add_upgrade_handler ?accept server route up
diff --git a/src/ws/tiny_httpd_ws.mli b/src/ws/tiny_httpd_ws.mli
index 44f48e9d..2bd30f70 100644
--- a/src/ws/tiny_httpd_ws.mli
+++ b/src/ws/tiny_httpd_ws.mli
@@ -4,9 +4,6 @@
for a websocket server. It has no additional dependencies.
*)
-open Tiny_httpd_server
-module IO = Tiny_httpd_io
-
type handler = Unix.sockaddr -> IO.Input.t -> IO.Output.t -> unit
(** Websocket handler *)
@@ -16,8 +13,8 @@ val upgrade : IO.Input.t -> IO.Output.t -> IO.Input.t * IO.Output.t
val add_route_handler :
?accept:(unit Request.t -> (unit, int * string) result) ->
?accept_ws_protocol:(string -> bool) ->
- Tiny_httpd_server.t ->
- (upgrade_handler, upgrade_handler) Route.t ->
+ Server.t ->
+ (Server.upgrade_handler, Server.upgrade_handler) Route.t ->
handler ->
unit
(** Add a route handler for a websocket endpoint.
diff --git a/tests/unit/dune b/tests/unit/dune
index c6944d20..7be0c4d9 100644
--- a/tests/unit/dune
+++ b/tests/unit/dune
@@ -2,4 +2,4 @@
(tests
(names t_util t_buf t_server)
(package tiny_httpd)
- (libraries tiny_httpd qcheck-core qcheck-core.runner test_util))
+ (libraries tiny_httpd.core qcheck-core qcheck-core.runner test_util))
diff --git a/tests/unit/t_buf.ml b/tests/unit/t_buf.ml
index 9ee0f685..68e0d20d 100644
--- a/tests/unit/t_buf.ml
+++ b/tests/unit/t_buf.ml
@@ -1,5 +1,5 @@
open Test_util
-open Tiny_httpd_buf
+open Tiny_httpd_core.Buf
let spf = Printf.sprintf
diff --git a/tests/unit/t_server.ml b/tests/unit/t_server.ml
index 56dd77ff..01b82eac 100644
--- a/tests/unit/t_server.ml
+++ b/tests/unit/t_server.ml
@@ -1,5 +1,5 @@
open Test_util
-open Tiny_httpd_server
+open Tiny_httpd_core
let () =
let q =
@@ -9,16 +9,20 @@ let () =
\r\n\
salutationsSOMEJUNK"
in
- let str = Tiny_httpd.Byte_stream.of_string q in
+ let str = IO.Input.of_string q in
let client_addr = Unix.(ADDR_INET (inet_addr_loopback, 1024)) in
- let r = Request.Internal_.parse_req_start ~client_addr ~get_time_s:(fun _ -> 0.) str in
+ let r =
+ Request.Private_.parse_req_start_exn ~client_addr ~buf:(Buf.create ())
+ ~get_time_s:(fun _ -> 0.)
+ str
+ in
match r with
| None -> failwith "should parse"
| Some req ->
- assert_eq (Some "coucou") (Headers.get "Host" req.Request.headers);
- assert_eq (Some "coucou") (Headers.get "host" req.Request.headers);
- assert_eq (Some "11") (Headers.get "content-length" req.Request.headers);
- assert_eq "hello" req.Request.path;
- let req = Request.Internal_.parse_body req str |> Request.read_body_full in
- assert_eq ~to_string:(fun s -> s) "salutations" req.Request.body;
+ assert_eq (Some "coucou") (Headers.get "Host" req.headers);
+ assert_eq (Some "coucou") (Headers.get "host" req.headers);
+ assert_eq (Some "11") (Headers.get "content-length" req.headers);
+ assert_eq "hello" req.path;
+ let req = Request.Private_.parse_body req str |> Request.read_body_full in
+ assert_eq ~to_string:(fun s -> s) "salutations" req.body;
()
diff --git a/tests/unit/t_util.ml b/tests/unit/t_util.ml
index 3ae913a4..db3c6758 100644
--- a/tests/unit/t_util.ml
+++ b/tests/unit/t_util.ml
@@ -1,33 +1,36 @@
open Test_util
-open Tiny_httpd_util
+open Tiny_httpd_core
+module U = Util
-let () = assert_eq "hello%20world" (percent_encode "hello world")
-let () = assert_eq "%23%25^%24%40^%40" (percent_encode "#%^$@^@")
+let () = assert_eq "hello%20world" (U.percent_encode "hello world")
+let () = assert_eq "%23%25^%24%40^%40" (U.percent_encode "#%^$@^@")
let () =
assert_eq "a%20ohm%2B5235%25%26%40%23%20---%20_"
- (percent_encode "a ohm+5235%&@# --- _")
+ (U.percent_encode "a ohm+5235%&@# --- _")
-let () = assert_eq (Some "?") (percent_decode @@ percent_encode "?")
+let () = assert_eq (Some "?") (U.percent_decode @@ U.percent_encode "?")
let () =
add_qcheck
@@ QCheck.Test.make ~count:1_000 ~long_factor:20 Q.string (fun s ->
String.iter (fun c -> Q.assume @@ is_ascii_char c) s;
- match percent_decode (percent_encode s) with
+ match U.percent_decode (U.percent_encode s) with
| Some s' -> s = s'
| None -> Q.Test.fail_report "invalid percent encoding")
-let () = assert_eq [ "a"; "b" ] (split_on_slash "/a/b")
-let () = assert_eq [ "coucou"; "lol" ] (split_on_slash "/coucou/lol")
-let () = assert_eq [ "a"; "b"; "c" ] (split_on_slash "/a/b//c/")
-let () = assert_eq [ "a"; "b" ] (split_on_slash "//a/b/")
-let () = assert_eq [ "a" ] (split_on_slash "/a//")
-let () = assert_eq [] (split_on_slash "/")
-let () = assert_eq [] (split_on_slash "//")
+let () = assert_eq [ "a"; "b" ] (U.split_on_slash "/a/b")
+let () = assert_eq [ "coucou"; "lol" ] (U.split_on_slash "/coucou/lol")
+let () = assert_eq [ "a"; "b"; "c" ] (U.split_on_slash "/a/b//c/")
+let () = assert_eq [ "a"; "b" ] (U.split_on_slash "//a/b/")
+let () = assert_eq [ "a" ] (U.split_on_slash "/a//")
+let () = assert_eq [] (U.split_on_slash "/")
+let () = assert_eq [] (U.split_on_slash "//")
let () =
- assert_eq ~cmp:eq_sorted (Ok [ "a", "b"; "c", "d" ]) (parse_query "a=b&c=d")
+ assert_eq ~cmp:eq_sorted (Ok [ "a", "b"; "c", "d" ]) (U.parse_query "a=b&c=d")
+
+let () = assert_eq (Ok [ "foo", "bar" ]) (U.parse_query "yolo#foo=bar")
let () =
add_qcheck
@@ -43,9 +46,9 @@ let () =
let s =
String.concat "&"
(List.map
- (fun (x, y) -> percent_encode x ^ "=" ^ percent_encode y)
+ (fun (x, y) -> U.percent_encode x ^ "=" ^ U.percent_encode y)
l)
in
- eq_sorted (Ok l) (parse_query s))
+ eq_sorted (Ok l) (U.parse_query s))
let () = run_qcheck_and_exit ()
diff --git a/tests/unit/util/dune b/tests/unit/util/dune
index fb97b15e..e2790663 100644
--- a/tests/unit/util/dune
+++ b/tests/unit/util/dune
@@ -2,4 +2,4 @@
(library
(name test_util)
(modules test_util)
- (libraries qcheck-core qcheck-core.runner))
+ (libraries logs qcheck-core qcheck-core.runner))
diff --git a/tests/unit/util/test_util.ml b/tests/unit/util/test_util.ml
index ae225b00..cd9c3dc5 100644
--- a/tests/unit/util/test_util.ml
+++ b/tests/unit/util/test_util.ml
@@ -29,3 +29,7 @@ let add_qcheck f = qchecks := f :: !qchecks
let run_qcheck_and_exit () : 'a =
exit @@ QCheck_base_runner.run_tests ~colors:false !qchecks
+
+let setup_logs_debug () =
+ Logs.set_reporter @@ Logs.format_reporter ();
+ Logs.set_level ~all:true @@ Some Logs.Debug
diff --git a/tiny_httpd.opam b/tiny_httpd.opam
index a5224f40..c144b511 100644
--- a/tiny_httpd.opam
+++ b/tiny_httpd.opam
@@ -15,6 +15,8 @@ depends: [
"seq"
"base-threads"
"result"
+ "hmap"
+ "iostream" {>= "0.2"}
"ocaml" {>= "4.08"}
"odoc" {with-doc}
"logs" {with-test}
diff --git a/tiny_httpd_camlzip.opam b/tiny_httpd_camlzip.opam
index e8a269f2..e28dde38 100644
--- a/tiny_httpd_camlzip.opam
+++ b/tiny_httpd_camlzip.opam
@@ -11,6 +11,7 @@ depends: [
"dune" {>= "2.9"}
"tiny_httpd" {= version}
"camlzip" {>= "1.06"}
+ "iostream-camlzip"
"logs" {with-test}
"odoc" {with-doc}
]
diff --git a/vendor/iostream b/vendor/iostream
new file mode 160000
index 00000000..668a7c22
--- /dev/null
+++ b/vendor/iostream
@@ -0,0 +1 @@
+Subproject commit 668a7c22c09d21293c9ce3fd8bc66b3080c525d2