mirror of
https://github.com/c-cube/tiny_httpd.git
synced 2025-12-09 04:35:32 -05:00
212 lines
5.8 KiB
OCaml
212 lines
5.8 KiB
OCaml
module Result = struct
|
|
include Result
|
|
|
|
let ( >>= ) :
|
|
type a b e. (a, e) result -> (a -> (b, e) result) -> (b, e) result =
|
|
fun r f ->
|
|
match r with
|
|
| Ok x -> f x
|
|
| Error _ as e -> e
|
|
end
|
|
|
|
open Result
|
|
|
|
module Meth = struct
|
|
type t =
|
|
[ `GET
|
|
| `POST
|
|
| `HEAD
|
|
| `PUT
|
|
| `DELETE
|
|
| `OPTIONS
|
|
| `TRACE
|
|
| `CONNECT
|
|
| `PATCH
|
|
| `Other of string ]
|
|
|
|
let to_string = function
|
|
| `GET -> "GET"
|
|
| `POST -> "POST"
|
|
| `HEAD -> "HEAD"
|
|
| `PUT -> "PUT"
|
|
| `DELETE -> "DELETE"
|
|
| `OPTIONS -> "OPTIONS"
|
|
| `TRACE -> "TRACE"
|
|
| `CONNECT -> "CONNECT"
|
|
| `PATCH -> "PATCH"
|
|
| `Other s -> s
|
|
|
|
let pp fmt t = Format.fprintf fmt "%s" (to_string t)
|
|
end
|
|
|
|
module Header = struct
|
|
type t = (string * string) list
|
|
|
|
let empty = []
|
|
|
|
let to_cmd t =
|
|
t
|
|
|> List.map (fun (k, v) -> [ "-H"; Printf.sprintf "%s: %s" k v ])
|
|
|> List.concat
|
|
|
|
let pp fmt t =
|
|
Format.pp_print_list ~pp_sep:Format.pp_print_newline
|
|
(fun fmt (k, v) -> Format.fprintf fmt "%s: %s\n" k v)
|
|
fmt t
|
|
end
|
|
|
|
module Response = struct
|
|
type t = Http.response = { code: int; headers: Header.t; body: string }
|
|
|
|
let default = { code = 0; headers = []; body = "" }
|
|
|
|
let of_stdout s =
|
|
let lexbuf = Lexing.from_string s in
|
|
try Ok (Http.response default lexbuf) with e -> Error e
|
|
|
|
let pp fmt t =
|
|
Format.fprintf fmt "{code=%d;@ headers=%a;@ body=\"%s\"}" t.code Header.pp
|
|
t.headers t.body
|
|
end
|
|
|
|
module Process_result = struct
|
|
type t = { status: Unix.process_status; stderr: string; stdout: string }
|
|
|
|
let pp_process_status fmt = function
|
|
| Unix.WEXITED n -> Format.fprintf fmt "Exit code %d" n
|
|
| Unix.WSIGNALED n -> Format.fprintf fmt "Signal %d" n
|
|
| Unix.WSTOPPED n -> Format.fprintf fmt "Stopped %d" n
|
|
|
|
let pp fmt t =
|
|
Format.fprintf fmt "{status=%a;@ stderr=\"%s\";@ stdout=\"%s\"}"
|
|
pp_process_status t.status t.stderr t.stdout
|
|
end
|
|
|
|
module Error = struct
|
|
type t =
|
|
| Invalid_request of string
|
|
| Bad_exit of Process_result.t
|
|
| Failed_to_read_response of exn * Process_result.t
|
|
| Exn of exn
|
|
|
|
let pp fmt = function
|
|
| Bad_exit p ->
|
|
Format.fprintf fmt "Non 0 exit code %a@.%a"
|
|
Process_result.pp_process_status p.Process_result.status
|
|
Process_result.pp p
|
|
| Failed_to_read_response (e, _) ->
|
|
Format.fprintf fmt "Couldn't read response:@ %s" (Printexc.to_string e)
|
|
| Invalid_request r -> Format.fprintf fmt "Invalid request: %s" r
|
|
| Exn e -> Format.fprintf fmt "Exception: %s" (Printexc.to_string e)
|
|
end
|
|
|
|
module Request = struct
|
|
type t = { meth: Meth.t; url: string; headers: Header.t; body: string }
|
|
|
|
let make ?(headers = Header.empty) ?(body = "") ~url ~meth () =
|
|
{ meth; url; headers; body }
|
|
|
|
let has_body t = String.length t.body > 0
|
|
|
|
let validate t =
|
|
if has_body t && List.mem t.meth [ `GET; `HEAD ] then
|
|
Error (Error.Invalid_request "No body is allowed with GET/HEAD methods")
|
|
else
|
|
Ok t
|
|
|
|
let to_cmd_args t =
|
|
List.concat
|
|
[
|
|
[ "-X"; Meth.to_string t.meth ];
|
|
Header.to_cmd t.headers;
|
|
[ t.url ];
|
|
(if has_body t then
|
|
[ "--data-binary"; "@-" ]
|
|
else
|
|
[]);
|
|
]
|
|
|
|
let pp fmt t =
|
|
Format.fprintf fmt
|
|
"{@ meth=%a;@ url=\"%s\";@ headers=\"%a\";@ body=\"%s\"@ }" Meth.pp t.meth
|
|
t.url Header.pp t.headers t.body
|
|
end
|
|
|
|
let result_of_process_result t =
|
|
match t.Process_result.status with
|
|
| Unix.WEXITED 0 -> Ok t
|
|
| _ -> Error (Error.Bad_exit t)
|
|
|
|
let run prog args stdin_str =
|
|
let stdout, stdin, stderr =
|
|
let prog = prog :: List.map Filename.quote args |> String.concat " " in
|
|
Unix.open_process_full prog [||]
|
|
in
|
|
if String.length stdin_str > 0 then output_string stdin stdin_str;
|
|
(try close_out stdin with _ -> ());
|
|
let stdout_fd = Unix.descr_of_in_channel stdout in
|
|
let stderr_fd = Unix.descr_of_in_channel stderr in
|
|
let in_buf, err_buf = Buffer.(create 128, create 128) in
|
|
let read_buf_len = 512 in
|
|
let read_buf = Bytes.create read_buf_len in
|
|
let input ch =
|
|
match input ch read_buf 0 read_buf_len with
|
|
| 0 -> Error `Eof
|
|
| s -> Ok s
|
|
in
|
|
let rec loop = function
|
|
| [] -> ()
|
|
| read_list ->
|
|
let can_read, _, _ = Unix.select read_list [] [] 1.0 in
|
|
let to_remove =
|
|
List.fold_left
|
|
(fun to_remove fh ->
|
|
let rr, buf =
|
|
if fh = stderr_fd then
|
|
input stderr, err_buf
|
|
else
|
|
input stdout, in_buf
|
|
in
|
|
match rr with
|
|
| Ok len ->
|
|
Buffer.add_subbytes buf read_buf 0 len;
|
|
to_remove
|
|
| Error `Eof -> fh :: to_remove)
|
|
[] can_read
|
|
in
|
|
read_list |> List.filter (fun fh -> not (List.mem fh to_remove)) |> loop
|
|
in
|
|
ignore (loop [ stdout_fd; stderr_fd ]);
|
|
let status = Unix.close_process_full (stdout, stdin, stderr) in
|
|
{
|
|
Process_result.status;
|
|
stdout = Buffer.contents in_buf;
|
|
stderr = Buffer.contents err_buf;
|
|
}
|
|
|
|
let run ?(exe = "curl") ?(args = []) req =
|
|
Request.validate req >>= fun req ->
|
|
let args = ("-si" :: Request.to_cmd_args req) @ args in
|
|
let res =
|
|
try result_of_process_result (run exe args req.Request.body)
|
|
with e -> Error (Error.Exn e)
|
|
in
|
|
res >>= fun res ->
|
|
match Response.of_stdout res.Process_result.stdout with
|
|
| Ok r -> Ok r
|
|
| Error e -> Error (Error.Failed_to_read_response (e, res))
|
|
|
|
let get ?exe ?args ?headers url =
|
|
run ?exe ?args (Request.make ?headers ~url ~meth:`GET ())
|
|
|
|
let head ?exe ?args ?headers url =
|
|
run ?exe ?args (Request.make ?headers ~url ~meth:`HEAD ())
|
|
|
|
let delete ?exe ?args ?headers url =
|
|
run ?exe ?args (Request.make ?headers ~url ~meth:`DELETE ())
|
|
|
|
let post ?exe ?args ?headers ?body url =
|
|
run ?exe ?args (Request.make ?body ?headers ~url ~meth:`POST ())
|
|
|
|
let put ?exe ?args ?headers ?body url =
|
|
run ?exe ?args (Request.make ?body ?headers ~url ~meth:`PUT ())
|