mirror of
https://github.com/c-cube/tiny_httpd.git
synced 2025-12-06 11:15:35 -05:00
feat: debug, and chunked encoding handled properly
(tested with local server, upload, and curl) close #1
This commit is contained in:
parent
bd3d4c591d
commit
f6efa9b703
3 changed files with 88 additions and 9 deletions
|
|
@ -8,6 +8,13 @@ type input_stream = {
|
||||||
exception Bad_req of int * string
|
exception Bad_req of int * string
|
||||||
let bad_reqf c fmt = Printf.ksprintf (fun s ->raise (Bad_req (c,s))) fmt
|
let bad_reqf c fmt = Printf.ksprintf (fun s ->raise (Bad_req (c,s))) fmt
|
||||||
|
|
||||||
|
let debug_ k =
|
||||||
|
if None<>Sys.getenv_opt "HTTP_DBG" then (
|
||||||
|
k (fun fmt ->
|
||||||
|
Printf.fprintf stdout "[thread %d]: " Thread.(id @@ self());
|
||||||
|
Printf.kfprintf (fun oc -> Printf.fprintf oc "\n%!") stdout fmt)
|
||||||
|
)
|
||||||
|
|
||||||
module Response_code = struct
|
module Response_code = struct
|
||||||
type t = int
|
type t = int
|
||||||
|
|
||||||
|
|
@ -53,6 +60,9 @@ end
|
||||||
|
|
||||||
module Headers = struct
|
module Headers = struct
|
||||||
type t = (string * string) list
|
type t = (string * string) list
|
||||||
|
let contains = List.mem_assoc
|
||||||
|
let get x h = try Some (List.assoc x h) with Not_found -> None
|
||||||
|
let set x y h = (x,y) :: List.filter (fun (k,_) -> k<>x) h
|
||||||
let pp out l =
|
let pp out l =
|
||||||
let pp_pair out (k,v) = Format.fprintf out "@[<h>%s: %s@]" k v in
|
let pp_pair out (k,v) = Format.fprintf out "@[<h>%s: %s@]" k v in
|
||||||
Format.fprintf out "@[<v>%a@]" (Format.pp_print_list pp_pair) l
|
Format.fprintf out "@[<v>%a@]" (Format.pp_print_list pp_pair) l
|
||||||
|
|
@ -92,6 +102,7 @@ module Request = struct
|
||||||
self.path self.body
|
self.path self.body
|
||||||
|
|
||||||
let read_body (is:input_stream) (n:int) : string =
|
let read_body (is:input_stream) (n:int) : string =
|
||||||
|
debug_ (fun k->k "read body of size %d" n);
|
||||||
if Bytes.length is.buf < n then (
|
if Bytes.length is.buf < n then (
|
||||||
is.buf <- Bytes.make n ' ';
|
is.buf <- Bytes.make n ' ';
|
||||||
);
|
);
|
||||||
|
|
@ -103,6 +114,40 @@ module Request = struct
|
||||||
done;
|
done;
|
||||||
Bytes.sub_string is.buf 0 n
|
Bytes.sub_string is.buf 0 n
|
||||||
|
|
||||||
|
let read_body_chunked (is:input_stream) : string =
|
||||||
|
debug_ (fun k->k "read body with chunked encoding");
|
||||||
|
let n = ref 0 in
|
||||||
|
let rec read_chunks () =
|
||||||
|
let line = input_line is.ic in
|
||||||
|
(* parse chunk length, ignore extensions *)
|
||||||
|
let chunk_size =
|
||||||
|
if String.trim line = "" then 0
|
||||||
|
else
|
||||||
|
try Scanf.sscanf line "%x %s@\r" (fun n _ext -> n)
|
||||||
|
with _ -> bad_reqf 400 "cannot read chunk size from line %S" line
|
||||||
|
in
|
||||||
|
debug_ (fun k->k "chunk size: %d" chunk_size);
|
||||||
|
if chunk_size = 0 then (
|
||||||
|
Bytes.sub_string is.buf 0 !n (* done *)
|
||||||
|
) else (
|
||||||
|
let new_size = chunk_size + !n in
|
||||||
|
(* resize buffer if needed *)
|
||||||
|
if Bytes.length is.buf < new_size then (
|
||||||
|
let new_buf = Bytes.make (new_size + 10) ' ' in
|
||||||
|
Bytes.blit is.buf 0 new_buf 0 !n;
|
||||||
|
is.buf <- new_buf;
|
||||||
|
);
|
||||||
|
while !n < new_size do
|
||||||
|
let read = input is.ic is.buf !n (new_size - !n) in
|
||||||
|
if read=0 then bad_reqf 400 "body is too short";
|
||||||
|
n := !n + read
|
||||||
|
done;
|
||||||
|
debug_ (fun k->k "read a chunk");
|
||||||
|
read_chunks()
|
||||||
|
)
|
||||||
|
in
|
||||||
|
read_chunks()
|
||||||
|
|
||||||
(* parse request, but not body (yet) *)
|
(* parse request, but not body (yet) *)
|
||||||
let parse_req_start (is:input_stream) : t option resp_result =
|
let parse_req_start (is:input_stream) : t option resp_result =
|
||||||
try
|
try
|
||||||
|
|
@ -113,9 +158,10 @@ module Request = struct
|
||||||
in
|
in
|
||||||
let meth = Meth.of_string meth in
|
let meth = Meth.of_string meth in
|
||||||
let headers = Headers.parse_ is in
|
let headers = Headers.parse_ is in
|
||||||
|
debug_ (fun k->k "got meth: %s" (Meth.to_string meth));
|
||||||
Ok (Some {meth; path; headers; body=""})
|
Ok (Some {meth; path; headers; body=""})
|
||||||
with
|
with
|
||||||
| End_of_file -> Ok None
|
| End_of_file | Sys_error _ -> Ok None
|
||||||
| Bad_req (c,s) -> Error (c,s)
|
| Bad_req (c,s) -> Error (c,s)
|
||||||
| e ->
|
| e ->
|
||||||
Error (400, Printexc.to_string e)
|
Error (400, Printexc.to_string e)
|
||||||
|
|
@ -124,10 +170,15 @@ module Request = struct
|
||||||
let parse_body_ (is:input_stream) (req:t) : t resp_result =
|
let parse_body_ (is:input_stream) (req:t) : t resp_result =
|
||||||
try
|
try
|
||||||
let body = match List.assoc "Content-Length" req.headers |> int_of_string with
|
let body = match List.assoc "Content-Length" req.headers |> int_of_string with
|
||||||
| exception Not_found -> ""
|
|
||||||
| exception _ -> bad_reqf 400 "invalid content-length"
|
|
||||||
| 0 -> ""
|
| 0 -> ""
|
||||||
| n -> read_body is n
|
| n -> read_body is n (* body of fixed size *)
|
||||||
|
| exception Not_found ->
|
||||||
|
begin match List.assoc "Transfer-Encoding" req.headers |> String.trim with
|
||||||
|
| "chunked" -> read_body_chunked is (* body sent by chunks *)
|
||||||
|
| s -> bad_reqf 500 "cannot handle transfer encoding: %s" s
|
||||||
|
| exception Not_found -> ""
|
||||||
|
end
|
||||||
|
| exception _ -> bad_reqf 400 "invalid content-length"
|
||||||
in
|
in
|
||||||
Ok {req with body}
|
Ok {req with body}
|
||||||
with
|
with
|
||||||
|
|
@ -144,10 +195,14 @@ module Response = struct
|
||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(* TODO: if query had ["Accept-Encoding", "chunked"], we cna reply with chunks,
|
||||||
|
if [body] was a stream|string instead of just a string *)
|
||||||
|
|
||||||
let make ?(headers=[]) ~code body : t =
|
let make ?(headers=[]) ~code body : t =
|
||||||
(* add 'content length' to response *)
|
(* add content length to response *)
|
||||||
let headers = List.filter (fun (k,_) -> k <> "Content-Length") headers in
|
let headers =
|
||||||
let headers = ("Content-Length", string_of_int (String.length body)) :: headers in
|
Headers.set "Content-Length" (string_of_int (String.length body)) headers
|
||||||
|
in
|
||||||
{ code; headers; body; }
|
{ code; headers; body; }
|
||||||
|
|
||||||
let make_ok ?headers body = make ~code:200 ?headers body
|
let make_ok ?headers body = make ~code:200 ?headers body
|
||||||
|
|
@ -227,6 +282,7 @@ let handle_client_ (self:t) (client_sock:Unix.file_descr) : unit =
|
||||||
let ph_handlers = self.path_handlers in
|
let ph_handlers = self.path_handlers in
|
||||||
let continue = ref true in
|
let continue = ref true in
|
||||||
while !continue && self.running do
|
while !continue && self.running do
|
||||||
|
debug_ (fun k->k "read next request");
|
||||||
match Request.parse_req_start is with
|
match Request.parse_req_start is with
|
||||||
| Ok None -> continue := false
|
| Ok None -> continue := false
|
||||||
| Error (c,s) ->
|
| Error (c,s) ->
|
||||||
|
|
@ -238,6 +294,7 @@ let handle_client_ (self:t) (client_sock:Unix.file_descr) : unit =
|
||||||
(* handle expectations *)
|
(* handle expectations *)
|
||||||
begin match List.assoc "Expect" req.Request.headers with
|
begin match List.assoc "Expect" req.Request.headers with
|
||||||
| "100-continue" ->
|
| "100-continue" ->
|
||||||
|
debug_ (fun k->k "send back: 100 CONTINUE");
|
||||||
Response.output_ oc (Response.make ~code:100 "");
|
Response.output_ oc (Response.make ~code:100 "");
|
||||||
| s -> bad_reqf 417 "unknown expectation %s" s
|
| s -> bad_reqf 417 "unknown expectation %s" s
|
||||||
| exception Not_found -> ()
|
| exception Not_found -> ()
|
||||||
|
|
@ -266,11 +323,14 @@ let handle_client_ (self:t) (client_sock:Unix.file_descr) : unit =
|
||||||
in
|
in
|
||||||
Response.output_ oc res
|
Response.output_ oc res
|
||||||
| exception Bad_req (code,s) ->
|
| exception Bad_req (code,s) ->
|
||||||
Response.output_ oc (Response.make ~code s)
|
Response.output_ oc (Response.make ~code s);
|
||||||
|
continue := false
|
||||||
| exception Sys_error _ ->
|
| exception Sys_error _ ->
|
||||||
continue := false; (* connection broken somehow *)
|
continue := false; (* connection broken somehow *)
|
||||||
Unix.close client_sock;
|
Unix.close client_sock;
|
||||||
done
|
done;
|
||||||
|
debug_ (fun k->k "done with client, exiting");
|
||||||
|
()
|
||||||
|
|
||||||
let run (self:t) : (unit,_) result =
|
let run (self:t) : (unit,_) result =
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ end
|
||||||
|
|
||||||
module Headers : sig
|
module Headers : sig
|
||||||
type t = (string * string) list
|
type t = (string * string) list
|
||||||
|
val get : string -> t -> string option
|
||||||
|
val set : string -> string -> t -> t
|
||||||
|
val contains : string -> t -> bool
|
||||||
val pp : Format.formatter -> t -> unit
|
val pp : Format.formatter -> t -> unit
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
|
|
||||||
module S = SimpleHTTPServer
|
module S = SimpleHTTPServer
|
||||||
|
|
||||||
|
let debug_ k =
|
||||||
|
if None<>Sys.getenv_opt "HTTP_DBG" then (
|
||||||
|
k (fun fmt -> Printf.kfprintf (fun oc -> k (Printf.fprintf oc)) stdout fmt)
|
||||||
|
)
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
let server = S.create () in
|
let server = S.create () in
|
||||||
(* say hello *)
|
(* say hello *)
|
||||||
|
|
@ -9,6 +14,17 @@ let () =
|
||||||
(* echo request *)
|
(* echo request *)
|
||||||
S.add_path_handler server
|
S.add_path_handler server
|
||||||
"/echo" (fun req () -> S.Response.make_ok (Format.asprintf "echo:@ %a@." S.Request.pp req));
|
"/echo" (fun req () -> S.Response.make_ok (Format.asprintf "echo:@ %a@." S.Request.pp req));
|
||||||
|
S.add_path_handler ~meth:`PUT server
|
||||||
|
"/upload/%s" (fun req path () ->
|
||||||
|
debug_ (fun k->k "start upload %S\n%!" path);
|
||||||
|
try
|
||||||
|
let oc = open_out @@ "/tmp/" ^ path in
|
||||||
|
output_string oc req.S.Request.body;
|
||||||
|
flush oc;
|
||||||
|
S.Response.make_ok "uploaded file"
|
||||||
|
with e ->
|
||||||
|
S.Response.make_error @@ "couldn't upload file " ^ Printexc.to_string e
|
||||||
|
);
|
||||||
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
|
Printf.printf "listening on http://%s:%d\n%!" (S.addr server) (S.port server);
|
||||||
match S.run server with
|
match S.run server with
|
||||||
| Ok () -> ()
|
| Ok () -> ()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue