package tiny_httpd
Minimal HTTP server using threads
Install
Dune Dependency
Authors
Maintainers
Sources
tiny_httpd-0.19.tbz
sha256=55b6eed7d2e41b9b2a282b344e01ca010abdba66cbbbbe5adab18303b9ff8360
sha512=98c599627b9cd98f641d389e022f9ddc025bfba838793267904b94a01b0b40b3b4cfd8f5a8d8214aaa947958e45cecbe1df751692a461f929802714d7af50e06
doc/src/tiny_httpd.unix/dir.ml.html
Source file dir.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
module S = Server module U = Util module Html = Tiny_httpd_html module Log = Log type dir_behavior = Index | Lists | Index_or_lists | Forbidden type config = { mutable download: bool; mutable dir_behavior: dir_behavior; mutable delete: bool; mutable upload: bool; mutable max_upload_size: int; _rest: hidden; } let default_config_ : config = { download = true; dir_behavior = Forbidden; delete = false; upload = false; max_upload_size = 10 * 1024 * 1024; _rest = (); } let default_config () = default_config_ let config ?(download = default_config_.download) ?(dir_behavior = default_config_.dir_behavior) ?(delete = default_config_.delete) ?(upload = default_config_.upload) ?(max_upload_size = default_config_.max_upload_size) () : config = { download; dir_behavior; delete; upload; max_upload_size; _rest = () } let contains_dot_dot s = try String.iteri (fun i c -> if c = '.' && i + 1 < String.length s && String.get s (i + 1) = '.' then raise Exit) s; false with Exit -> true (* Human readable size *) let human_size (x : int) : string = if x >= 1_000_000_000 then Printf.sprintf "%d.%dG" (x / 1_000_000_000) (x / 1_000_000 mod 1_000_000) else if x >= 1_000_000 then Printf.sprintf "%d.%dM" (x / 1_000_000) (x / 1000 mod 1_000) else if x >= 1_000 then Printf.sprintf "%d.%dk" (x / 1000) (x / 100 mod 100) else Printf.sprintf "%db" x let header_html = "Content-Type", "text/html" let ( // ) = Filename.concat let encode_path s = U.percent_encode ~skip:(function | '/' -> true | _ -> false) s let _decode_path s = match U.percent_decode s with | Some s -> s | None -> s let s = String.length s > 0 && s.[0] = '.' module type VFS = sig val descr : string val is_directory : string -> bool val contains : string -> bool val list_dir : string -> string array val delete : string -> unit val create : string -> (bytes -> int -> int -> unit) * (unit -> unit) val read_file_content : string -> IO.Input.t val file_size : string -> int option val file_mtime : string -> float option end type vfs = (module VFS) let vfs_of_dir (top : string) : vfs = let module M = struct let descr = top let ( // ) = Filename.concat let is_directory f = Sys.is_directory (top // f) let contains f = Sys.file_exists (top // f) let list_dir f = Sys.readdir (top // f) let read_file_content f = let fpath = top // f in match Unix.stat fpath with | { st_kind = Unix.S_REG; _ } -> let ic = Unix.(openfile fpath [ O_RDONLY ] 0) in let closed = ref false in 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 = let oc = open_out_bin (top // f) in let write = output oc in let close () = close_out oc in write, close let delete f = Sys.remove (top // f) let file_size f = try Some (Unix.stat (top // f)).Unix.st_size with _ -> None let file_mtime f = try Some (Unix.stat (top // f)).Unix.st_mtime with _ -> None end in (module M) let html_list_dir (module VFS : VFS) ~prefix ~parent d : Html.elt = let entries = VFS.list_dir d in Array.sort String.compare entries; let open Html in (* TODO: breadcrumbs for the path, each element a link to the given ancestor dir *) let head = head [] [ title [] [ txtf "list directory %S" VFS.descr ]; meta [ A.charset "utf-8" ]; ] in let = ref 0 in Array.iter (fun f -> if is_hidden f then incr n_hidden) entries; let file_to_elt f : elt option = if not @@ contains_dot_dot (d // f) then ( let fpath = d // f in if not @@ VFS.contains fpath then Some (li [] [ txtf "%s [invalid file]" f ]) else ( let size = match VFS.file_size fpath with | Some f -> Printf.sprintf " (%s)" @@ human_size f | None -> "" in Some (li' [] [ sub_e @@ a [ A.href ("/" // prefix // fpath) ] [ txt f ]; (if VFS.is_directory fpath then sub_e @@ txt "[dir]" else sub_empty); sub_e @@ txt size; ]) ) ) else None in let body = body' [] [ sub_e @@ h2 [] [ txtf "Index of %S" d ]; (match parent with | None -> sub_empty | Some p -> sub_e @@ a [ A.href (encode_path ("/" // prefix // p)) ] [ txt "(parent directory)" ]); sub_e @@ ul' [] [ (if !n_hidden > 0 then sub_e @@ details' [] [ sub_e @@ summary [] [ txtf "(%d hidden files)" !n_hidden ]; sub_seq (seq_of_array entries |> Seq.filter_map (fun f -> if is_hidden f then file_to_elt f else None)); ] else sub_empty); sub_seq (seq_of_array entries |> Seq.filter_map (fun f -> if not (is_hidden f) then file_to_elt f else None)); ]; ] in html [] [ head; body ] (* @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 route () = if prefix = "" then Route.rest_of_path_urlencoded else 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 Response.fail_raise ~code:403 "invalid path in delete" else 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 _ _ -> 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 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.Request.path -> Error (403, "invalid path (contains '..')") | _ -> Ok ()) (fun path req -> let write, close = try VFS.create path with e -> Response.fail_raise ~code:403 "cannot upload to %S: %s" path (Printexc.to_string e) in let req = Request.limit_body_size ~bytes:(Bytes.create 4096) ~max_size:config.max_upload_size req in IO.Input.iter write req.body; close (); Log.debug (fun k -> k "dir: done uploading file to %S" path); Response.make_raw ~code:201 "upload successful") else S.add_route_handler server ~meth:`PUT (route ()) (fun _ _ -> Response.make_raw ~code:405 "upload not allowed"); if config.download then S.add_route_handler server ~meth:`GET (route ()) (fun path req -> Log.debug (fun k -> k "dir: download path=%S" path); let mtime = lazy (match VFS.file_mtime path with | None -> Response.fail_raise ~code:403 "Cannot access file" | Some t -> Printf.sprintf "mtime: %.4f" t) in 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" 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)); 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 let parent = if Filename.basename path <> "." then Some parent else None in match config.dir_behavior with | (Index | Index_or_lists) when VFS.contains (path // "index.html") -> (* 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); 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 Response.make_string ~headers:[ header_html; "ETag", Lazy.force mtime ] (Ok body) | Forbidden | Index -> Response.make_raw ~code:405 "listing dir not allowed" ) else ( try let mime_type = (* 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 = Mime_.mime_of_path (top // path) in [ "content-type", ty ] ) else [] in let stream = VFS.read_file_content path in Response.make_raw_stream ~headers:(mime_type @ [ "Etag", Lazy.force mtime ]) ~code:200 stream with e -> let bt = Printexc.get_raw_backtrace () in let msg = Printexc.to_string e in Log.error (fun k -> k "dir.get failed: %s@.%s" msg (Printexc.raw_backtrace_to_string bt)); Response.fail ~code:500 "error while reading file: %s" msg )) else S.add_route_handler server ~meth:`GET (route ()) (fun _ _ -> Response.make_raw ~code:405 "download not allowed"); () let add_vfs ~config ~vfs ~prefix server : unit = add_vfs_ ~on_fs:false ~top:"." ~config ~prefix ~vfs server let add_dir_path ~config ~dir ~prefix server : unit = add_vfs_ ~on_fs:true ~top:dir ~config ~prefix ~vfs:(vfs_of_dir dir) server module Embedded_fs = struct module Str_map = Map.Make (String) type t = { mtime: float; mutable entries: entry Str_map.t } and entry = File of { content: string; mtime: float } | Dir of t let create ?(mtime = Unix.gettimeofday ()) () : t = { mtime; entries = Str_map.empty } let split_path_ (path : string) : string list * string = let basename = Filename.basename path in let dirname = Filename.dirname path |> String.split_on_char '/' |> List.filter (function | "" | "." -> false | _ -> true) in dirname, basename let add_file ?mtime (self : t) ~path content : unit = let mtime = match mtime with | Some t -> t | None -> self.mtime in let dir_path, basename = split_path_ path in if List.mem ".." dir_path then invalid_arg "add_file: '..' is not allowed"; let rec loop self dir = match dir with | [] -> self.entries <- Str_map.add basename (File { mtime; content }) self.entries | d :: ds -> let sub = match Str_map.find d self.entries with | Dir sub -> sub | File _ -> invalid_arg (Printf.sprintf "in path %S, %S is a file, not a directory" path d) | exception Not_found -> let sub = create ~mtime:self.mtime () in self.entries <- Str_map.add d (Dir sub) self.entries; sub in loop sub ds in loop self dir_path (* find entry *) let find_ self path : entry option = let dir_path, basename = split_path_ path in let rec loop self dir_name = match dir_name with | [] -> (try Some (Str_map.find basename self.entries) with _ -> None) | d :: ds -> (match Str_map.find d self.entries with | exception Not_found -> None | File _ -> None | Dir sub -> loop sub ds) in if path = "" then Some (Dir self) else loop self dir_path let to_vfs self : vfs = let module M = struct let descr = "Embedded_fs" let file_mtime p = match find_ self p with | Some (File { mtime; _ }) -> Some mtime | Some (Dir _) -> Some self.mtime | _ -> None let file_size p = match find_ self p with | Some (File { content; _ }) -> Some (String.length content) | _ -> None let contains p = Log.debug (fun k -> k "vfs: contains %S" p); match find_ self p with | Some _ -> true | None -> false let is_directory p = match find_ self p with | Some (Dir _) -> true | _ -> false let read_file_content p = match find_ self p with | Some (File { content; _ }) -> IO.Input.of_string content | _ -> failwith (Printf.sprintf "no such file: %S" p) let list_dir p = Log.debug (fun k -> k "vfs: list dir %S" p); match find_ self p with | Some (Dir sub) -> Str_map.fold (fun sub _ acc -> sub :: acc) sub.entries [] |> Array.of_list | _ -> failwith (Printf.sprintf "no such directory: %S" p) let create _ = failwith "Embedded_fs is read-only" let delete _ = failwith "Embedded_fs is read-only" end in (module M) end
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>