add more doc

This commit is contained in:
Simon Cruanes 2019-11-18 22:03:01 -06:00
parent 3048bfcc82
commit 95248a132c

View file

@ -1,12 +1,34 @@
(** {1 Tiny Http Server}
This library implements a very simple, basic HTTP/1.1 server using blocking
IOs and threads. Basic routing based on {!Scanf} is provided for convenience,
so that several handlers can be registered.
It is possible to use a thread pool, see {!create}'s argument [new_thread].
*)
type stream = { type stream = {
is_fill_buf: unit -> (bytes * int * int); is_fill_buf: unit -> (bytes * int * int);
(** See the current slice of the internal buffer as [bytes, i, len],
where the slice is [bytes[i] .. [bytes[i+len-1]]].
Can block to refill the buffer if there is currently no content.
If [len=0] then there is no more data. *)
is_consume: int -> unit; is_consume: int -> unit;
(** Consume n bytes from the buffer. This should only be called with [n <= len]
after a call to [is_fill_buf] that returns a slice of length [len]. *)
is_close: unit -> unit; is_close: unit -> unit;
(** Close the stream. *)
} }
(** A buffer input stream, with a view into the current buffer (or refill if empty), (** A buffered stream, with a view into the current buffer (or refill if empty),
and a function to consume [n] bytes *) and a function to consume [n] bytes.
See {!Buf_} for more details. *)
(** {2 Tiny buffer implementation} *) (** {2 Tiny buffer implementation}
These buffers are used to avoid allocating too many byte arrays when
processing streams and parsing requests.
*)
module Buf_ : sig module Buf_ : sig
type t type t
val size : t -> int val size : t -> int
@ -15,19 +37,36 @@ module Buf_ : sig
val contents : t -> string val contents : t -> string
end end
(** {2 Generic stream of data} *) (** {2 Generic stream of data}
Streams are used to represent a series of bytes that can arrive progressively.
For example, an uploaded file will be sent as a series of chunks. *)
module Stream_ : sig module Stream_ : sig
type t = stream type t = stream
val close : t -> unit val close : t -> unit
val of_chan : in_channel -> t val of_chan : in_channel -> t
(** Make a buffered stream from the given channel. *)
val of_chan_close_noerr : in_channel -> t val of_chan_close_noerr : in_channel -> t
(** Same as {!of_chan} but the [close] method will never fail. *)
val of_bytes : ?i:int -> ?len:int -> bytes -> t val of_bytes : ?i:int -> ?len:int -> bytes -> t
(** A stream that just returns the slice of bytes starting from [i]
and of length [len]. *)
val with_file : string -> (t -> 'a) -> 'a val with_file : string -> (t -> 'a) -> 'a
(** Open a file with given name, and obtain an input stream *) (** Open a file with given name, and obtain an input stream
on its content. When the function returns, the stream (and file) are closed. *)
val read_line : ?buf:Buf_.t -> t -> string val read_line : ?buf:Buf_.t -> t -> string
(** Read a line from the stream.
@param buf a buffer to (re)use. Its content will be cleared. *)
val read_all : ?buf:Buf_.t -> t -> string val read_all : ?buf:Buf_.t -> t -> string
(** Read the whole stream into a string.
@param buf a buffer to (re)use. Its content will be cleared. *)
end end
module Meth : sig module Meth : sig
@ -38,6 +77,10 @@ module Meth : sig
| `HEAD | `HEAD
| `DELETE | `DELETE
] ]
(** 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 pp : Format.formatter -> t -> unit
val to_string : t -> string val to_string : t -> string
@ -45,13 +88,32 @@ end
module Headers : sig module Headers : sig
type t = (string * string) list 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 get : ?f:(string->string) -> string -> t -> string option val get : ?f:(string->string) -> string -> t -> string option
(** [get k headers] looks for the header field with key [k].
@param f if provided, will transform the value before it is returned. *)
val set : string -> string -> t -> t 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 val remove : string -> t -> t
(** Remove the key from the headers, if present. *)
val contains : string -> t -> bool val contains : string -> t -> bool
(** Is there a header with the given key? *)
val pp : Format.formatter -> t -> unit val pp : Format.formatter -> t -> unit
(** Pretty print the headers. *)
end end
(** {2 HTTP request}
A request sent by a client. *)
module Request : sig module Request : sig
type 'body t = { type 'body t = {
meth: Meth.t; meth: Meth.t;
@ -59,63 +121,110 @@ module Request : sig
path: string; path: string;
body: 'body; body: 'body;
} }
(** A request with method, path, headers, and a body.
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}. *)
val pp : Format.formatter -> string t -> unit val pp : Format.formatter -> string t -> unit
(** Pretty print the request and its body *)
val pp_ : Format.formatter -> _ t -> unit val pp_ : Format.formatter -> _ t -> unit
(** Pretty print the request without its body *)
val headers : _ t -> Headers.t val headers : _ t -> Headers.t
val get_header : ?f:(string->string) -> _ t -> string -> string option val get_header : ?f:(string->string) -> _ t -> string -> string option
val get_header_int : _ t -> string -> int option val get_header_int : _ t -> string -> int option
val set_header : 'a t -> string -> string -> 'a t val set_header : 'a t -> string -> string -> 'a t
val meth : _ t -> Meth.t val meth : _ t -> Meth.t
val path : _ t -> string val path : _ t -> string
val body : 'b t -> 'b val body : 'b t -> 'b
val read_body_full : stream t -> string t val read_body_full : stream t -> string t
(** Read the whole body into a string. Potentially blocking. *)
end end
(** {2 Response code} *)
module Response_code : sig module Response_code : sig
type t = int type t = int
(** A standard HTTP code.
https://tools.ietf.org/html/rfc7231#section-6 *)
val ok : t val ok : t
(** The code [200] *)
val not_found : t val not_found : t
(** The code [404] *)
val descr : t -> string val descr : t -> string
(** A description of some of the error codes.
NOTE: this is not complete (yet). *)
end end
(** {2 Response}
A response sent back to a client. *)
module Response : sig module Response : sig
type body = [`String of string | `Stream of stream] type body = [`String of string | `Stream of stream]
(** Body of a response, either as a simple string,
or a stream of bytes. *)
type t = { type t = {
code: Response_code.t; code: Response_code.t; (** HTTP response code. See {!Response_code}. *)
headers: Headers.t; headers: Headers.t; (** Headers of the reply. Some will be set by [Tiny_httpd] automatically. *)
body: body; body: body; (** Body of the response. Can be empty. *)
} }
(** A response. *)
val make_raw : val make_raw :
?headers:Headers.t -> ?headers:Headers.t ->
code:Response_code.t -> code:Response_code.t ->
string -> string ->
t t
(** Make a response from its raw components, with a string body.
Use [""] to not send a body at all. *)
val make_raw_stream : val make_raw_stream :
?headers:Headers.t -> ?headers:Headers.t ->
code:Response_code.t -> code:Response_code.t ->
stream -> stream ->
t t
(** Same as {!make_raw} but with a stream body. The body will be sent with
the chunked transfer-encoding. *)
val make : val make :
?headers:Headers.t -> ?headers:Headers.t ->
(body, Response_code.t * string) result -> t (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 : val make_string :
?headers:Headers.t -> ?headers:Headers.t ->
(string, Response_code.t * string) result -> t (string, Response_code.t * string) result -> t
(** Same as {!make} but with a string body. *)
val make_stream : val make_stream :
?headers:Headers.t -> ?headers:Headers.t ->
(stream, Response_code.t * string) result -> t (stream, Response_code.t * string) result -> t
(** Same as {!make} but with a stream body. *)
val fail : ?headers:Headers.t -> code:int -> val fail : ?headers:Headers.t -> code:int ->
('a, unit, string, t) format4 -> 'a ('a, unit, string, t) format4 -> 'a
(** Make the current request fail with the given code and message. (** Make the current request fail with the given code and message.
Example: [fail ~code:404 "oh noes, %s not found" "waldo"] Example: [fail ~code:404 "oh noes, %s not found" "waldo"].
*) *)
val fail_raise : code:int -> ('a, unit, string, 'b) format4 -> 'a val fail_raise : code:int -> ('a, unit, string, 'b) format4 -> 'a
@ -125,9 +234,11 @@ module Response : sig
*) *)
val pp : Format.formatter -> t -> unit val pp : Format.formatter -> t -> unit
(** Pretty print the response. *)
end end
type t type t
(** A HTTP server. See {!create} for more details. *)
val create : val create :
?masksigpipe:bool -> ?masksigpipe:bool ->
@ -136,10 +247,28 @@ val create :
?port:int -> ?port:int ->
unit -> unit ->
t t
(** TODO: document *) (** Create a new webserver.
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 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.
@param addr the address (IPv4) to listen on. Default ["127.0.0.1"].
@param port to listen on. Default [8080].
*)
val addr : t -> string val addr : t -> string
(** Address on which the server listen. *)
val port : t -> int val port : t -> int
(** Port on which the server listen. *)
val add_decode_request_cb : val add_decode_request_cb :
t -> t ->
@ -147,6 +276,8 @@ val add_decode_request_cb :
(** Add a callback for every request. (** Add a callback for every request.
The callback can provide a stream transformer and a new request (with The callback can provide a stream transformer and a new request (with
modified headers, typically). modified headers, typically).
A possible use is to handle decompression by looking for a [Transfer-Encoding]
header and returning a stream transformer that decompresses on the fly.
*) *)
val add_encode_response_cb: val add_encode_response_cb:
@ -160,7 +291,10 @@ val add_encode_response_cb:
val set_top_handler : t -> (string Request.t -> Response.t) -> unit val set_top_handler : t -> (string Request.t -> Response.t) -> unit
(** Setup a handler called by default. (** Setup a handler called by default.
If not installed, unhandled paths will return a 404 not found. *)
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. *)
val add_path_handler : val add_path_handler :
?accept:(unit Request.t -> (unit, Response_code.t * string) result) -> ?accept:(unit Request.t -> (unit, Response_code.t * string) result) ->
@ -172,14 +306,22 @@ val add_path_handler :
(** [add_path_handler server "/some/path/%s@/%d/" f] (** [add_path_handler server "/some/path/%s@/%d/" f]
calls [f request "foo" 42 ()] when a request with path "some/path/foo/42/" calls [f request "foo" 42 ()] when a request with path "some/path/foo/42/"
is received. is received.
This uses {!Scanf}'s splitting, which has some gotchas (in particular, This uses {!Scanf}'s splitting, which has some gotchas (in particular,
["%s"] is eager, so it's generally necessary to delimit its ["%s"] is eager, so it's generally necessary to delimit its
scope with a ["@/"] delimiter. The "@" before a character indicates it's scope with a ["@/"] delimiter. The "@" before a character indicates it's
a separator. a separator.
@param meth if provided, only accept requests with the given method
@param accept should return [true] if the given request (before its body Note that the handlers are called in the reverse order of their addition,
is read) should be accepted, [false] if it's to be rejected (e.g. because 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). 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.
*) *)
val stop : t -> unit val stop : t -> unit
@ -189,7 +331,10 @@ val stop : t -> unit
val run : t -> (unit, exn) result val run : t -> (unit, exn) result
(** Run the main loop of the server, listening on a socket (** Run the main loop of the server, listening on a socket
described at the server's creation time, using [new_thread] to described at the server's creation time, using [new_thread] to
start a thread for each new client. *) start a thread for each new client.
This returns [Ok ()] if the server exits gracefully, or [Error e] if
it exits with an error. *)
(**/**) (**/**)