package tiny_httpd

  1. Overview
  2. Docs
Minimal HTTP server using threads

Install

dune-project
 Dependency

Authors

Maintainers

Sources

tiny_httpd-0.22.tbz
sha256=09fb93125c26b99f8714d0060dd3fb972046cde3ff935626d495b25e80543d8e
sha512=5751a0ac007c8cad11255d08b4915c8a8fb82ad33d28e58809ef1e29dc8c8f2fceb0dd15ecbe1fda5b5db2edbbc9479fa1d0690a4a25949e1d1b375ea13bd1a9

doc/src/tiny_httpd.core/headers.ml.html

Source file headers.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
open Common_

type t = (string * string) list

let empty = []

(* [Char.lowercase_ascii] but easier to inline *)
let[@inline] lower_char_ = function
  | 'A' .. 'Z' as c -> Char.unsafe_chr (Char.code c + 32)
  | c -> c

(** Are these two header names equal? This is case insensitive *)
let equal_name_ (s1 : string) (s2 : string) : bool =
  String.length s1 = String.length s2
  &&
  try
    for i = 0 to String.length s1 - 1 do
      let c1 = String.unsafe_get s1 i |> lower_char_ in
      let c2 = String.unsafe_get s2 i |> lower_char_ in
      if c1 <> c2 then raise_notrace Exit
    done;
    true
  with Exit -> false

let contains name headers =
  List.exists (fun (n, _) -> equal_name_ name n) headers

let list_contains_nocase_ name l = List.exists (equal_name_ name) l

let rec get_exn ?(f = fun x -> x) x h =
  match h with
  | [] -> raise Not_found
  | (k, v) :: _ when equal_name_ x k -> f v
  | _ :: tl -> get_exn ~f x tl

let get ?(f = fun x -> x) x h =
  try Some (get_exn ~f x h) with Not_found -> None

let remove x h = List.filter (fun (k, _) -> not (equal_name_ k x)) h

let set x y h =
  let h =
    if contains x h then
      remove x h
    else
      h
  in
  (x, y) :: h

let pp out l =
  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

(* token = 1*tchar
   tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_"
            / "`" / "|" / "~" / DIGIT / ALPHA ; any VCHAR, except delimiters
   Reference: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 *)
let is_tchar = function
  | '0' .. '9'
  | 'a' .. 'z'
  | 'A' .. 'Z'
  | '!' | '#' | '$' | '%' | '&' | '\'' | '*' | '+' | '-' | '.' | '^' | '_' | '`'
  | '|' | '~' ->
    true
  | _ -> false

let for_all pred s =
  try
    String.iter (fun c -> if not (pred c) then raise Exit) s;
    true
  with Exit -> false

let parse_line_ (line : string) : _ result =
  try
    let i =
      try String.index line ':'
      with Not_found -> failwith "invalid header, missing ':'"
    in
    let k = String.sub line 0 i in
    if not (for_all is_tchar k) then
      failwith (Printf.sprintf "Invalid header key: %S" k);
    let v =
      String.sub line (i + 1) (String.length line - i - 1) |> String.trim
    in
    Ok (k, v)
  with Failure msg -> Error msg

open struct
  let nodup_ = [ "content-length"; "host"; "transfer-encoding" ]
  let is_nodup_ k = list_contains_nocase_ k nodup_
end

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;
    match IO.Input.read_line_using_opt ~buf bs with
    | None -> raise End_of_file
    | Some "" -> assert false
    | Some "\r" -> acc
    | 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

      if is_nodup_ k && contains k acc then
        bad_reqf 400 "header %S is duplicated" k;

      loop ((k, v) :: acc) (count + 1) (total_size + line_len)
  in

  let headers = loop [] 0 0 in
  if
    contains "content-length" headers
    && get "transfer-encoding" headers = Some "chunked"
  then
    bad_reqf 400 "cannot specify both chunked encoding and content-length";

  headers