mirror of
https://github.com/c-cube/tiny_httpd.git
synced 2026-03-16 10:19:58 -04:00
Compare commits
5 commits
8cf59ffed1
...
d9c0f94869
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9c0f94869 | ||
|
|
a98dd9b767 | ||
|
|
f80df7f6a7 | ||
|
|
d40f87f07b | ||
|
|
4b4fd2afe1 |
23 changed files with 142 additions and 310 deletions
28
.github/workflows/format.yml
vendored
28
.github/workflows/format.yml
vendored
|
|
@ -1,28 +0,0 @@
|
|||
name: format
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: format
|
||||
strategy:
|
||||
matrix:
|
||||
ocaml-compiler:
|
||||
- '5.3'
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
- name: Use OCaml ${{ matrix.ocaml-compiler }}
|
||||
uses: ocaml/setup-ocaml@v3
|
||||
with:
|
||||
ocaml-compiler: ${{ matrix.ocaml-compiler }}
|
||||
dune-cache: true
|
||||
allow-prerelease-opam: true
|
||||
|
||||
- run: opam install ocamlformat.0.27.0
|
||||
- run: opam exec -- make format-check
|
||||
|
||||
36
.github/workflows/gh-pages.yml
vendored
Normal file
36
.github/workflows/gh-pages.yml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
name: github pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use OCaml
|
||||
uses: ocaml/setup-ocaml@v3
|
||||
with:
|
||||
ocaml-compiler: 5.03.x
|
||||
dune-cache: true
|
||||
allow-prerelease-opam: true
|
||||
|
||||
- name: Deps
|
||||
run: opam install odig tiny_httpd tiny_httpd_camlzip tiny_httpd_eio
|
||||
|
||||
- name: Build
|
||||
run: opam exec -- odig odoc --cache-dir=_doc/ tiny_httpd tiny_httpd_camlzip tiny_httpd_eio
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./_doc/html
|
||||
destination_dir: .
|
||||
enable_jekyll: false
|
||||
#keep_files: true
|
||||
|
||||
20
.github/workflows/main.yml
vendored
20
.github/workflows/main.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
#- macos-latest
|
||||
#- windows-latest
|
||||
ocaml-compiler:
|
||||
- 4.13.x
|
||||
- 4.08.x
|
||||
- 4.14.x
|
||||
- 5.03.x
|
||||
|
||||
|
|
@ -38,15 +38,7 @@ jobs:
|
|||
|
||||
- run: opam install ./tiny_httpd.opam ./tiny_httpd_camlzip.opam --deps-only --with-test
|
||||
|
||||
- name: Build (OCaml 4.x)
|
||||
run: opam exec -- dune build @install -p tiny_httpd,tiny_httpd_camlzip
|
||||
if: ${{ !startsWith(matrix.ocaml-compiler, '5.') }}
|
||||
|
||||
- name: Build (OCaml 5.x, includes eio)
|
||||
run: |
|
||||
opam install ./tiny_httpd.opam ./tiny_httpd_eio.opam --deps-only --with-test
|
||||
opam exec -- dune build @install -p tiny_httpd,tiny_httpd_camlzip,tiny_httpd_eio
|
||||
if: ${{ startsWith(matrix.ocaml-compiler, '5.') }}
|
||||
- run: opam exec -- dune build @install -p tiny_httpd,tiny_httpd_camlzip,tiny_httpd_eio
|
||||
|
||||
- run: opam exec -- dune build @src/runtest @examples/runtest @tests/runtest -p tiny_httpd
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
|
|
@ -58,10 +50,4 @@ jobs:
|
|||
|
||||
- run: opam install logs magic-mime -y
|
||||
|
||||
- name: Final build (OCaml 4.x)
|
||||
run: opam exec -- dune build @install -p tiny_httpd,tiny_httpd_camlzip
|
||||
if: ${{ !startsWith(matrix.ocaml-compiler, '5.') }}
|
||||
|
||||
- name: Final build (OCaml 5.x, includes eio)
|
||||
run: opam exec -- dune build @install -p tiny_httpd,tiny_httpd_camlzip,tiny_httpd_eio
|
||||
if: ${{ startsWith(matrix.ocaml-compiler, '5.') }}
|
||||
- run: opam exec -- dune build @install -p tiny_httpd,tiny_httpd_camlzip,tiny_httpd_eio
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,5 +3,3 @@ _build
|
|||
_opam
|
||||
*.install
|
||||
.merlin
|
||||
todo.md
|
||||
*.tmp
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@
|
|||
result
|
||||
hmap
|
||||
(iostream (>= 0.2))
|
||||
(ocaml (>= 4.13))
|
||||
(ocaml (>= 4.08))
|
||||
(odoc :with-doc)
|
||||
(logs :with-test)
|
||||
(conf-libcurl :with-test)
|
||||
(ptime :with-test)
|
||||
(qcheck-core (and (>= 0.91) :with-test))))
|
||||
(qcheck-core (and (>= 0.9) :with-test))))
|
||||
|
||||
(package
|
||||
(name tiny_httpd_camlzip)
|
||||
|
|
@ -46,6 +46,5 @@
|
|||
(depends
|
||||
(tiny_httpd (= :version))
|
||||
(eio (and (>= 1.0) (< 2.0)))
|
||||
base-unix
|
||||
(logs :with-test)
|
||||
(odoc :with-doc)))
|
||||
|
|
|
|||
|
|
@ -73,10 +73,10 @@
|
|||
; produce an embedded FS
|
||||
|
||||
(library
|
||||
(name echo_vfs)
|
||||
(modules vfs)
|
||||
(wrapped false)
|
||||
(libraries tiny_httpd))
|
||||
(name echo_vfs)
|
||||
(modules vfs)
|
||||
(wrapped false)
|
||||
(libraries tiny_httpd))
|
||||
|
||||
(rule
|
||||
(targets vfs.ml)
|
||||
|
|
|
|||
|
|
@ -69,10 +69,12 @@ let middleware_stat () : Server.Middleware.t * (unit -> string) =
|
|||
|
||||
let middleware_trace : Server.Middleware.t =
|
||||
fun (h : Server.Middleware.handler) req ~resp ->
|
||||
let _sp = Trace.enter_span ~__FILE__ ~__LINE__ "http.handle" in
|
||||
let _sp =
|
||||
Trace.enter_manual_toplevel_span ~__FILE__ ~__LINE__ "http.handle"
|
||||
in
|
||||
let new_resp (r : Response.t) =
|
||||
Trace.add_data_to_span _sp [ "http.code", `Int r.code ];
|
||||
Trace.exit_span _sp;
|
||||
Trace.add_data_to_manual_span _sp [ "http.code", `Int r.code ];
|
||||
Trace.exit_manual_span _sp;
|
||||
resp r
|
||||
in
|
||||
h req ~resp:new_resp
|
||||
|
|
|
|||
|
|
@ -83,13 +83,8 @@ let parse_line_ (line : string) : _ result =
|
|||
Ok (k, v)
|
||||
with Failure msg -> Error msg
|
||||
|
||||
let parse_ ~(buf : Buf.t) ?(max_headers = 100) ?(max_header_size = 16 * 1024)
|
||||
?(max_total_size = 256 * 1024) (bs : IO.Input.t) : t =
|
||||
let rec loop acc count total_size =
|
||||
if count >= max_headers then
|
||||
bad_reqf 431 "too many headers (max: %d)" max_headers;
|
||||
if total_size >= max_total_size then
|
||||
bad_reqf 431 "headers too large (max: %d bytes)" max_total_size;
|
||||
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 "" -> assert false
|
||||
|
|
@ -97,15 +92,12 @@ let parse_ ~(buf : Buf.t) ?(max_headers = 100) ?(max_header_size = 16 * 1024)
|
|||
| Some line when line.[String.length line - 1] <> '\r' ->
|
||||
bad_reqf 400 "bad header line, not ended in CRLF"
|
||||
| Some line ->
|
||||
let line_len = String.length line in
|
||||
if line_len > max_header_size then
|
||||
bad_reqf 431 "header too large (max: %d bytes)" max_header_size;
|
||||
let k, v =
|
||||
match parse_line_ line with
|
||||
| Ok r -> r
|
||||
| Error msg ->
|
||||
bad_reqf 400 "invalid header line: %s\nline is: %S" msg line
|
||||
in
|
||||
loop ((k, v) :: acc) (count + 1) (total_size + line_len)
|
||||
loop ((k, v) :: acc)
|
||||
in
|
||||
loop [] 0 0
|
||||
loop []
|
||||
|
|
|
|||
|
|
@ -34,14 +34,7 @@ val pp : Format.formatter -> t -> unit
|
|||
|
||||
(**/*)
|
||||
|
||||
val parse_ :
|
||||
buf:Buf.t ->
|
||||
?max_headers:int ->
|
||||
?max_header_size:int ->
|
||||
?max_total_size:int ->
|
||||
IO.Input.t ->
|
||||
t
|
||||
|
||||
val parse_ : buf:Buf.t -> IO.Input.t -> t
|
||||
val parse_line_ : string -> (string * string, string) result
|
||||
|
||||
(**/*)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ let descr = function
|
|||
| 411 -> "Length required"
|
||||
| 413 -> "Payload too large"
|
||||
| 417 -> "Expectation failed"
|
||||
| 431 -> "Request Header Fields Too Large"
|
||||
| 500 -> "Internal server error"
|
||||
| 501 -> "Not implemented"
|
||||
| 503 -> "Service unavailable"
|
||||
|
|
|
|||
|
|
@ -55,9 +55,6 @@ val to_string : _ t -> string
|
|||
|
||||
val to_url : ('a, string) t -> 'a
|
||||
|
||||
(** [to_url route args] takes a route, and turns it into a URL path.
|
||||
@since NEXT_RELEASE *)
|
||||
|
||||
module Private_ : sig
|
||||
val eval : string list -> ('a, 'b) t -> 'a -> 'b option
|
||||
end
|
||||
|
|
|
|||
12
src/eio/dune
12
src/eio/dune
|
|
@ -1,6 +1,8 @@
|
|||
|
||||
(library
|
||||
(name tiny_httpd_eio)
|
||||
(public_name tiny_httpd_eio)
|
||||
(synopsis "An EIO-based backend for Tiny_httpd")
|
||||
(flags :standard -safe-string -warn-error -a+8)
|
||||
(libraries tiny_httpd eio eio.unix))
|
||||
(name tiny_httpd_eio)
|
||||
(public_name tiny_httpd_eio)
|
||||
(synopsis "An EIO-based backend for Tiny_httpd")
|
||||
(flags :standard -safe-string -warn-error -a+8)
|
||||
(libraries tiny_httpd eio eio.unix))
|
||||
|
||||
|
|
|
|||
|
|
@ -31,10 +31,8 @@ let eio_sock_addr_to_unix (a : Eio.Net.Sockaddr.stream) : Unix.sockaddr =
|
|||
| `Tcp (h, p) -> Unix.ADDR_INET (eio_ipaddr_to_unix h, p)
|
||||
| `Unix s -> Unix.ADDR_UNIX s
|
||||
|
||||
let ic_of_flow ~closed ~buf_pool:ic_pool (flow : _ Eio.Net.stream_socket) :
|
||||
IO.Input.t =
|
||||
let ic_of_flow ~buf_pool:ic_pool (flow : _ Eio.Net.stream_socket) : IO.Input.t =
|
||||
let cstruct = Pool.Raw.acquire ic_pool in
|
||||
let sent_shutdown = ref false in
|
||||
|
||||
object
|
||||
inherit Iostream.In_buf.t_from_refill ()
|
||||
|
|
@ -54,22 +52,15 @@ let ic_of_flow ~closed ~buf_pool:ic_pool (flow : _ Eio.Net.stream_socket) :
|
|||
sl.len <- n
|
||||
|
||||
method close () =
|
||||
if not !closed then (
|
||||
closed := true;
|
||||
Pool.Raw.release ic_pool cstruct
|
||||
);
|
||||
if not !sent_shutdown then (
|
||||
sent_shutdown := true;
|
||||
Eio.Flow.shutdown flow `Receive
|
||||
)
|
||||
Pool.Raw.release ic_pool cstruct;
|
||||
Eio.Flow.shutdown flow `Receive
|
||||
end
|
||||
|
||||
let oc_of_flow ~closed ~buf_pool:oc_pool (flow : _ Eio.Net.stream_socket) :
|
||||
IO.Output.t =
|
||||
let oc_of_flow ~buf_pool:oc_pool (flow : _ Eio.Net.stream_socket) : IO.Output.t
|
||||
=
|
||||
(* write buffer *)
|
||||
let wbuf : Cstruct.t = Pool.Raw.acquire oc_pool in
|
||||
let offset = ref 0 in
|
||||
let sent_shutdown = ref false in
|
||||
|
||||
object (self)
|
||||
method flush () : unit =
|
||||
|
|
@ -100,14 +91,8 @@ let oc_of_flow ~closed ~buf_pool:oc_pool (flow : _ Eio.Net.stream_socket) :
|
|||
if !offset = Cstruct.length wbuf then self#flush ()
|
||||
|
||||
method close () =
|
||||
if not !closed then (
|
||||
closed := true;
|
||||
Pool.Raw.release oc_pool wbuf
|
||||
);
|
||||
if not !sent_shutdown then (
|
||||
sent_shutdown := true;
|
||||
Eio.Flow.shutdown flow `Send
|
||||
)
|
||||
Pool.Raw.release oc_pool wbuf;
|
||||
Eio.Flow.shutdown flow `Send
|
||||
end
|
||||
|
||||
let io_backend ?addr ?port ?unix_sock ?max_connections ?max_buf_pool_size
|
||||
|
|
@ -133,8 +118,7 @@ let io_backend ?addr ?port ?unix_sock ?max_connections ?max_buf_pool_size
|
|||
let module M = struct
|
||||
let init_addr () = addr
|
||||
let init_port () = port
|
||||
let clock = Eio.Stdenv.clock stdenv
|
||||
let get_time_s () = Eio.Time.now clock
|
||||
let get_time_s () = Unix.gettimeofday ()
|
||||
let max_connections = get_max_connection_ ?max_connections ()
|
||||
|
||||
let pool_size =
|
||||
|
|
@ -143,7 +127,7 @@ let io_backend ?addr ?port ?unix_sock ?max_connections ?max_buf_pool_size
|
|||
| None -> min 4096 (max_connections * 2)
|
||||
|
||||
let cstruct_pool =
|
||||
Pool.create ~max_size:pool_size
|
||||
Pool.create ~max_size:max_connections
|
||||
~mk_item:(fun () -> Cstruct.create buf_size)
|
||||
()
|
||||
|
||||
|
|
@ -153,7 +137,6 @@ let io_backend ?addr ?port ?unix_sock ?max_connections ?max_buf_pool_size
|
|||
(fun ~after_init ~handle () : unit ->
|
||||
let running = Atomic.make true in
|
||||
let active_conns = Atomic.make 0 in
|
||||
let sem = Eio.Semaphore.make max_connections in
|
||||
|
||||
Eio.Switch.on_release sw (fun () -> Atomic.set running false);
|
||||
let net = Eio.Stdenv.net stdenv in
|
||||
|
|
@ -165,26 +148,17 @@ let io_backend ?addr ?port ?unix_sock ?max_connections ?max_buf_pool_size
|
|||
sockaddr
|
||||
in
|
||||
|
||||
(* Resolve actual address/port (important for port 0) *)
|
||||
let actual_addr, actual_port =
|
||||
match Eio.Net.listening_addr sock with
|
||||
| `Tcp (_, p) -> addr, p
|
||||
| `Unix s -> Printf.sprintf "unix:%s" s, 0
|
||||
in
|
||||
|
||||
let tcp_server : IO.TCP_server.t =
|
||||
{
|
||||
running = (fun () -> Atomic.get running);
|
||||
stop =
|
||||
(fun () ->
|
||||
Atomic.set running false;
|
||||
(* Backstop: fail the switch after 60s if handlers don't complete *)
|
||||
Eio.Fiber.fork_daemon ~sw (fun () ->
|
||||
Eio.Time.sleep clock 60.0;
|
||||
if Eio.Switch.get_error sw |> Option.is_none then
|
||||
Eio.Switch.fail sw Exit;
|
||||
`Stop_daemon));
|
||||
endpoint = (fun () -> actual_addr, actual_port);
|
||||
Eio.Switch.fail sw Exit);
|
||||
endpoint =
|
||||
(fun () ->
|
||||
(* TODO: find the real port *)
|
||||
addr, port);
|
||||
active_connections = (fun () -> Atomic.get active_conns);
|
||||
}
|
||||
in
|
||||
|
|
@ -192,50 +166,33 @@ let io_backend ?addr ?port ?unix_sock ?max_connections ?max_buf_pool_size
|
|||
after_init tcp_server;
|
||||
|
||||
while Atomic.get running do
|
||||
match Eio.Net.accept ~sw sock with
|
||||
| exception (Eio.Cancel.Cancelled _ | Eio.Io _)
|
||||
when not (Atomic.get running) ->
|
||||
(* Socket closed or switch cancelled during shutdown; exit loop *)
|
||||
()
|
||||
| conn, client_addr ->
|
||||
(* Acquire semaphore BEFORE spawning a fiber so we
|
||||
bound the number of in-flight fibers. *)
|
||||
Eio.Semaphore.acquire sem;
|
||||
Eio.Fiber.fork ~sw (fun () ->
|
||||
let@ () =
|
||||
Fun.protect ~finally:(fun () ->
|
||||
Log.debug (fun k ->
|
||||
k "Tiny_httpd_eio: client handler returned");
|
||||
Atomic.decr active_conns;
|
||||
Eio.Semaphore.release sem;
|
||||
try Eio.Flow.close conn with Eio.Io _ -> ())
|
||||
in
|
||||
(try
|
||||
Eio_unix.Fd.use_exn "setsockopt" (Eio_unix.Net.fd conn)
|
||||
(fun fd -> Unix.setsockopt fd Unix.TCP_NODELAY true)
|
||||
with Unix.Unix_error _ -> ());
|
||||
Atomic.incr active_conns;
|
||||
let ic_closed = ref false in
|
||||
let oc_closed = ref false in
|
||||
let ic =
|
||||
ic_of_flow ~closed:ic_closed ~buf_pool:cstruct_pool conn
|
||||
in
|
||||
let oc =
|
||||
oc_of_flow ~closed:oc_closed ~buf_pool:cstruct_pool conn
|
||||
in
|
||||
Eio.Net.accept_fork ~sw
|
||||
~on_error:(fun exn ->
|
||||
Log.error (fun k ->
|
||||
k "error in client handler: %s" (Printexc.to_string exn)))
|
||||
sock
|
||||
(fun flow client_addr ->
|
||||
Atomic.incr active_conns;
|
||||
let@ () =
|
||||
Fun.protect ~finally:(fun () ->
|
||||
Log.debug (fun k ->
|
||||
k "Tiny_httpd_eio: client handler returned");
|
||||
Atomic.decr active_conns)
|
||||
in
|
||||
let ic = ic_of_flow ~buf_pool:cstruct_pool flow in
|
||||
let oc = oc_of_flow ~buf_pool:cstruct_pool flow in
|
||||
|
||||
Log.debug (fun k ->
|
||||
k "handling client on %a…" Eio.Net.Sockaddr.pp
|
||||
client_addr);
|
||||
let client_addr_unix = eio_sock_addr_to_unix client_addr in
|
||||
try handle.handle ~client_addr:client_addr_unix ic oc
|
||||
with exn ->
|
||||
let bt = Printexc.get_raw_backtrace () in
|
||||
Log.error (fun k ->
|
||||
k "Client handler for %a failed with %s\n%s"
|
||||
Eio.Net.Sockaddr.pp client_addr
|
||||
(Printexc.to_string exn)
|
||||
(Printexc.raw_backtrace_to_string bt)))
|
||||
Log.debug (fun k ->
|
||||
k "handling client on %a…" Eio.Net.Sockaddr.pp client_addr);
|
||||
let client_addr_unix = eio_sock_addr_to_unix client_addr in
|
||||
try handle.handle ~client_addr:client_addr_unix ic oc
|
||||
with exn ->
|
||||
let bt = Printexc.get_raw_backtrace () in
|
||||
Log.error (fun k ->
|
||||
k "Client handler for %a failed with %s\n%s"
|
||||
Eio.Net.Sockaddr.pp client_addr
|
||||
(Printexc.to_string exn)
|
||||
(Printexc.raw_backtrace_to_string bt)))
|
||||
done);
|
||||
}
|
||||
end in
|
||||
|
|
|
|||
|
|
@ -43,27 +43,6 @@ let contains_dot_dot s =
|
|||
false
|
||||
with Exit -> true
|
||||
|
||||
(* Check if string [s] starts with prefix [pre] *)
|
||||
let string_prefix ~pre s =
|
||||
let len_pre = String.length pre in
|
||||
String.length s >= len_pre && String.sub s 0 len_pre = pre
|
||||
|
||||
(* Check if a path is safe (doesn't escape root directory).
|
||||
Only needed for real filesystem access. *)
|
||||
let is_path_safe ~root_canonical ~path =
|
||||
try
|
||||
let full_path = Filename.concat root_canonical path in
|
||||
let path_canonical = Unix.realpath full_path in
|
||||
string_prefix ~pre:root_canonical path_canonical
|
||||
with Unix.Unix_error _ ->
|
||||
(* If realpath fails (e.g., file doesn't exist for uploads),
|
||||
check parent directory *)
|
||||
(try
|
||||
let parent = Filename.dirname (Filename.concat root_canonical path) in
|
||||
let parent_canonical = Unix.realpath parent in
|
||||
string_prefix ~pre:root_canonical parent_canonical
|
||||
with Unix.Unix_error _ -> false)
|
||||
|
||||
(* Human readable size *)
|
||||
let human_size (x : int) : string =
|
||||
if x >= 1_000_000_000 then
|
||||
|
|
@ -227,17 +206,6 @@ let html_list_dir (module VFS : VFS) ~prefix ~parent d : Html.elt =
|
|||
(* @param on_fs: if true, we assume the file exists on the FS *)
|
||||
let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
|
||||
: unit =
|
||||
let root_canonical =
|
||||
if on_fs then (
|
||||
try Some (Unix.realpath top) with _ -> None
|
||||
) else
|
||||
None
|
||||
in
|
||||
let check_path path =
|
||||
match root_canonical with
|
||||
| Some root -> is_path_safe ~root_canonical:root ~path
|
||||
| None -> not (contains_dot_dot path)
|
||||
in
|
||||
let route () =
|
||||
if prefix = "" then
|
||||
Route.rest_of_path_urlencoded
|
||||
|
|
@ -246,7 +214,7 @@ let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
|
|||
in
|
||||
if config.delete then
|
||||
S.add_route_handler server ~meth:`DELETE (route ()) (fun path _req ->
|
||||
if not (check_path path) then
|
||||
if contains_dot_dot path then
|
||||
Response.fail_raise ~code:403 "invalid path in delete"
|
||||
else
|
||||
Response.make_string
|
||||
|
|
@ -265,7 +233,7 @@ let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
|
|||
| Some n when n > config.max_upload_size ->
|
||||
Error
|
||||
(403, "max upload size is " ^ string_of_int config.max_upload_size)
|
||||
| Some _ when not (check_path req.Request.path) ->
|
||||
| Some _ when contains_dot_dot req.Request.path ->
|
||||
Error (403, "invalid path (contains '..')")
|
||||
| _ -> Ok ())
|
||||
(fun path req ->
|
||||
|
|
@ -296,7 +264,7 @@ let add_vfs_ ~on_fs ~top ~config ~vfs:((module VFS : VFS) as vfs) ~prefix server
|
|||
| None -> Response.fail_raise ~code:403 "Cannot access file"
|
||||
| Some t -> Printf.sprintf "mtime: %.4f" t)
|
||||
in
|
||||
if not (check_path path) then
|
||||
if contains_dot_dot path then
|
||||
Response.fail ~code:403 "Path is forbidden"
|
||||
else if not (VFS.contains path) then
|
||||
Response.fail ~code:404 "File not found"
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
(public_name tiny_httpd.ws)
|
||||
(synopsis "Websockets for tiny_httpd")
|
||||
(private_modules common_ws_ utils_)
|
||||
(flags :standard -w -32 -open Tiny_httpd_core)
|
||||
(flags :standard -open Tiny_httpd_core)
|
||||
(foreign_stubs
|
||||
(language c)
|
||||
(names tiny_httpd_ws_stubs)
|
||||
|
|
|
|||
|
|
@ -1,36 +1,15 @@
|
|||
open Common_ws_
|
||||
|
||||
module With_lock = struct
|
||||
type t = { with_lock: 'a. (unit -> 'a) -> 'a }
|
||||
type builder = unit -> t
|
||||
|
||||
let default_builder : builder =
|
||||
fun () ->
|
||||
let mutex = Mutex.create () in
|
||||
{
|
||||
with_lock =
|
||||
(fun f ->
|
||||
Mutex.lock mutex;
|
||||
try
|
||||
let x = f () in
|
||||
Mutex.unlock mutex;
|
||||
x
|
||||
with e ->
|
||||
Mutex.unlock mutex;
|
||||
raise e);
|
||||
}
|
||||
end
|
||||
|
||||
type handler = unit Request.t -> IO.Input.t -> IO.Output.t -> unit
|
||||
|
||||
module Frame_type = struct
|
||||
type t = int
|
||||
|
||||
let _continuation : t = 0
|
||||
let _text : t = 1
|
||||
let continuation : t = 0
|
||||
let text : t = 1
|
||||
let binary : t = 2
|
||||
let _close : t = 8
|
||||
let _ping : t = 9
|
||||
let close : t = 8
|
||||
let ping : t = 9
|
||||
let pong : t = 10
|
||||
|
||||
let show = function
|
||||
|
|
@ -73,10 +52,10 @@ module Writer = struct
|
|||
mutable offset: int; (** number of bytes already in [buf] *)
|
||||
oc: IO.Output.t;
|
||||
mutable closed: bool;
|
||||
mutex: With_lock.t;
|
||||
mutex: Mutex.t;
|
||||
}
|
||||
|
||||
let create ?(buf_size = 16 * 1024) ~with_lock ~oc () : t =
|
||||
let create ?(buf_size = 16 * 1024) ~oc () : t =
|
||||
{
|
||||
header = Header.create ();
|
||||
header_buf = Bytes.create 16;
|
||||
|
|
@ -84,9 +63,19 @@ module Writer = struct
|
|||
offset = 0;
|
||||
oc;
|
||||
closed = false;
|
||||
mutex = with_lock;
|
||||
mutex = Mutex.create ();
|
||||
}
|
||||
|
||||
let[@inline] with_mutex_ (self : t) f =
|
||||
Mutex.lock self.mutex;
|
||||
try
|
||||
let x = f () in
|
||||
Mutex.unlock self.mutex;
|
||||
x
|
||||
with e ->
|
||||
Mutex.unlock self.mutex;
|
||||
raise e
|
||||
|
||||
let[@inline] close self = self.closed <- true
|
||||
let int_of_bool : bool -> int = Obj.magic
|
||||
|
||||
|
|
@ -132,7 +121,7 @@ module Writer = struct
|
|||
()
|
||||
|
||||
(** Max fragment size: send 16 kB at a time *)
|
||||
let _max_fragment_size = 16 * 1024
|
||||
let max_fragment_size = 16 * 1024
|
||||
|
||||
let[@inline never] really_output_buf_ (self : t) =
|
||||
self.header.fin <- true;
|
||||
|
|
@ -153,7 +142,7 @@ module Writer = struct
|
|||
if self.offset = Bytes.length self.buf then really_output_buf_ self
|
||||
|
||||
let send_pong (self : t) : unit =
|
||||
let@ () = self.mutex.with_lock in
|
||||
let@ () = with_mutex_ self in
|
||||
self.header.fin <- true;
|
||||
self.header.ty <- Frame_type.pong;
|
||||
self.header.payload_len <- 0;
|
||||
|
|
@ -162,7 +151,7 @@ module Writer = struct
|
|||
write_header_ self
|
||||
|
||||
let output_char (self : t) c : unit =
|
||||
let@ () = self.mutex.with_lock in
|
||||
let@ () = with_mutex_ self in
|
||||
let cap = Bytes.length self.buf - self.offset in
|
||||
(* make room for [c] *)
|
||||
if cap = 0 then really_output_buf_ self;
|
||||
|
|
@ -172,7 +161,7 @@ module Writer = struct
|
|||
if cap = 1 then really_output_buf_ self
|
||||
|
||||
let output (self : t) buf i len : unit =
|
||||
let@ () = self.mutex.with_lock in
|
||||
let@ () = with_mutex_ self in
|
||||
let i = ref i in
|
||||
let len = ref len in
|
||||
while !len > 0 do
|
||||
|
|
@ -190,7 +179,7 @@ module Writer = struct
|
|||
flush_if_full self
|
||||
|
||||
let flush self : unit =
|
||||
let@ () = self.mutex.with_lock in
|
||||
let@ () = with_mutex_ self in
|
||||
flush_ self
|
||||
end
|
||||
|
||||
|
|
@ -401,8 +390,8 @@ module Reader = struct
|
|||
)
|
||||
end
|
||||
|
||||
let upgrade ?(with_lock = With_lock.default_builder ()) ic oc : _ * _ =
|
||||
let writer = Writer.create ~with_lock ~oc () in
|
||||
let upgrade ic oc : _ * _ =
|
||||
let writer = Writer.create ~oc () in
|
||||
let reader = Reader.create ~ic ~writer () in
|
||||
let ws_ic : IO.Input.t =
|
||||
object
|
||||
|
|
@ -429,7 +418,6 @@ let upgrade ?(with_lock = With_lock.default_builder ()) ic oc : _ * _ =
|
|||
upgrade handler *)
|
||||
module Make_upgrade_handler (X : sig
|
||||
val accept_ws_protocol : string -> bool
|
||||
val with_lock : With_lock.builder
|
||||
val handler : handler
|
||||
end) : Server.UPGRADE_HANDLER with type handshake_state = unit Request.t =
|
||||
struct
|
||||
|
|
@ -474,8 +462,7 @@ struct
|
|||
try Ok (handshake_ req) with Bad_req s -> Error s
|
||||
|
||||
let handle_connection req ic oc =
|
||||
let with_lock = X.with_lock () in
|
||||
let ws_ic, ws_oc = upgrade ~with_lock ic oc in
|
||||
let ws_ic, ws_oc = upgrade ic oc in
|
||||
try X.handler req ws_ic ws_oc
|
||||
with Close_connection ->
|
||||
Log.debug (fun k -> k "websocket: requested to close the connection");
|
||||
|
|
@ -483,11 +470,9 @@ struct
|
|||
end
|
||||
|
||||
let add_route_handler ?accept ?(accept_ws_protocol = fun _ -> true) ?middlewares
|
||||
?(with_lock = With_lock.default_builder) (server : Server.t) route
|
||||
(f : handler) : unit =
|
||||
(server : Server.t) route (f : handler) : unit =
|
||||
let module M = Make_upgrade_handler (struct
|
||||
let handler = f
|
||||
let with_lock = with_lock
|
||||
let accept_ws_protocol = accept_ws_protocol
|
||||
end) in
|
||||
let up : Server.upgrade_handler = (module M) in
|
||||
|
|
|
|||
|
|
@ -3,36 +3,11 @@
|
|||
This sub-library ([tiny_httpd.ws]) exports a small implementation for a
|
||||
websocket server. It has no additional dependencies. *)
|
||||
|
||||
(** Synchronization primitive used to allow both the reader to reply to "ping",
|
||||
and the handler to send messages, without stepping on each other's toes.
|
||||
|
||||
@since NEXT_RELEASE *)
|
||||
module With_lock : sig
|
||||
type t = { with_lock: 'a. (unit -> 'a) -> 'a }
|
||||
(** A primitive to run the callback in a critical section where others cannot
|
||||
run at the same time.
|
||||
|
||||
The default is a mutex, but that works poorly with thread pools so it's
|
||||
possible to use a semaphore or a cooperative mutex instead. *)
|
||||
|
||||
type builder = unit -> t
|
||||
|
||||
val default_builder : builder
|
||||
(** Lock using [Mutex]. *)
|
||||
end
|
||||
|
||||
type handler = unit Request.t -> IO.Input.t -> IO.Output.t -> unit
|
||||
(** Websocket handler *)
|
||||
|
||||
val upgrade :
|
||||
?with_lock:With_lock.t ->
|
||||
IO.Input.t ->
|
||||
IO.Output.t ->
|
||||
IO.Input.t * IO.Output.t
|
||||
(** Upgrade a byte stream to the websocket framing protocol.
|
||||
@param with_lock
|
||||
if provided, use this to prevent reader and writer to compete on sending
|
||||
frames. since NEXT_RELEASE. *)
|
||||
val upgrade : IO.Input.t -> IO.Output.t -> IO.Input.t * IO.Output.t
|
||||
(** Upgrade a byte stream to the websocket framing protocol. *)
|
||||
|
||||
exception Close_connection
|
||||
(** Exception that can be raised from IOs inside the handler, when the
|
||||
|
|
@ -42,7 +17,6 @@ val add_route_handler :
|
|||
?accept:(unit Request.t -> (unit, int * string) result) ->
|
||||
?accept_ws_protocol:(string -> bool) ->
|
||||
?middlewares:Server.Head_middleware.t list ->
|
||||
?with_lock:With_lock.builder ->
|
||||
Server.t ->
|
||||
(Server.upgrade_handler, Server.upgrade_handler) Route.t ->
|
||||
handler ->
|
||||
|
|
@ -50,11 +24,7 @@ val add_route_handler :
|
|||
(** Add a route handler for a websocket endpoint.
|
||||
@param accept_ws_protocol
|
||||
decides whether this endpoint accepts the websocket protocol sent by the
|
||||
client. Default accepts everything.
|
||||
@param with_lock
|
||||
if provided, use this to synchronize writes between the frame reader
|
||||
(replies "pong" to "ping") and the handler emitting writes. since
|
||||
NEXT_RELEASE. *)
|
||||
client. Default accepts everything. *)
|
||||
|
||||
(**/**)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
(tests
|
||||
(names t_util t_buf t_server t_io t_response t_headers)
|
||||
(names t_util t_buf t_server t_io t_response)
|
||||
(package tiny_httpd)
|
||||
(libraries tiny_httpd.core qcheck-core qcheck-core.runner test_util))
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
open Tiny_httpd_core
|
||||
|
||||
(* Test that header size limits are enforced *)
|
||||
let test_header_too_large () =
|
||||
(* Create a header that's larger than 16KB *)
|
||||
let large_value = String.make 20000 'x' in
|
||||
let q =
|
||||
"GET / HTTP/1.1\r\nHost: example.com\r\nX-Large: " ^ large_value
|
||||
^ "\r\n\r\n"
|
||||
in
|
||||
let str = IO.Input.of_string q in
|
||||
let client_addr = Unix.(ADDR_INET (inet_addr_loopback, 1024)) in
|
||||
let buf = Buf.create () in
|
||||
try
|
||||
let _ =
|
||||
Request.Private_.parse_req_start_exn ~client_addr ~buf
|
||||
~get_time_s:(fun _ -> 0.)
|
||||
str
|
||||
in
|
||||
failwith "should have failed with 431"
|
||||
with Tiny_httpd_core.Response.Bad_req (431, _) -> () (* expected *)
|
||||
|
||||
let () = test_header_too_large ()
|
||||
|
|
@ -40,7 +40,7 @@ let () = assert_eq (Ok [ "foo", "bar" ]) (U.parse_query "yolo#foo=bar")
|
|||
let () =
|
||||
add_qcheck
|
||||
@@ QCheck.Test.make ~name:__LOC__ ~long_factor:20 ~count:1_000
|
||||
Q.(list_small (pair string string))
|
||||
Q.(small_list (pair string string))
|
||||
(fun l ->
|
||||
List.iter
|
||||
(fun (a, b) ->
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ let () =
|
|||
@@ QCheck.Test.make ~count:10_000
|
||||
Q.(
|
||||
triple
|
||||
(bytes_size (Gen.return 4))
|
||||
(option nat_small)
|
||||
(bytes_size Gen.(0 -- 6000))
|
||||
(bytes_of_size (Gen.return 4))
|
||||
(option small_nat)
|
||||
(bytes_of_size Gen.(0 -- 6000))
|
||||
(* |> Q.add_stat ("b.size", fun (_k, b) -> Bytes.length b) *)
|
||||
|> Q.add_shrink_invariant (fun (k, _, _) -> Bytes.length k = 4))
|
||||
(fun (key, mask_offset, b) ->
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ depends: [
|
|||
"result"
|
||||
"hmap"
|
||||
"iostream" {>= "0.2"}
|
||||
"ocaml" {>= "4.13"}
|
||||
"ocaml" {>= "4.08"}
|
||||
"odoc" {with-doc}
|
||||
"logs" {with-test}
|
||||
"conf-libcurl" {with-test}
|
||||
"ptime" {with-test}
|
||||
"qcheck-core" {>= "0.91" & with-test}
|
||||
"qcheck-core" {>= "0.9" & with-test}
|
||||
]
|
||||
depopts: [
|
||||
"logs"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ depends: [
|
|||
"dune" {>= "3.2"}
|
||||
"tiny_httpd" {= version}
|
||||
"eio" {>= "1.0" & < "2.0"}
|
||||
"base-unix"
|
||||
"logs" {with-test}
|
||||
"odoc" {with-doc}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue