package http_async

  1. Overview
  2. Docs

Source file parser.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
open Core

exception Fail of Error.t
exception Partial

let tchar_map =
  Array.init 256 ~f:(fun idx ->
    match Char.of_int_exn idx with
    | '0' .. '9'
    | 'a' .. 'z'
    | 'A' .. 'Z'
    | '!'
    | '#'
    | '$'
    | '%'
    | '&'
    | '\''
    | '*'
    | '+'
    | '-'
    | '.'
    | '^'
    | '_'
    | '`'
    | '|'
    | '~' -> true
    | _ -> false)
;;

module Source = struct
  type t =
    { buffer : Bigstring.t
    ; mutable pos : int
    ; upper_bound : int
    }

  let[@inline always] unsafe_get t idx = Bigstring.get t.buffer (t.pos + idx)
  let[@inline always] unsafe_advance t count = t.pos <- t.pos + count
  let[@inline always] length t = t.upper_bound - t.pos
  let[@inline always] is_empty t = t.pos = t.upper_bound

  let[@inline always] to_string t ~pos ~len =
    let b = Bytes.create len in
    Bigstring.To_bytes.unsafe_blit
      ~src:t.buffer
      ~dst:b
      ~src_pos:(t.pos + pos)
      ~dst_pos:0
      ~len;
    Bytes.unsafe_to_string ~no_mutation_while_string_reachable:b
  ;;

  let[@inline always] to_iovec t ~pos ~len =
    Core_unix.IOVec.of_bigstring t.buffer ~pos:(t.pos + pos) ~len
  ;;

  let[@inline always] is_space = function
    | ' ' | '\012' | '\n' | '\r' | '\t' -> true
    | _ -> false
  ;;

  let[@inline always] to_string_trim t ~pos ~len =
    let last = ref (t.pos + len - 1) in
    let pos = ref (t.pos + pos) in
    while is_space (Bigstring.get t.buffer !pos) do
      incr pos
    done;
    while is_space (Bigstring.get t.buffer !last) do
      decr last
    done;
    let len = !last - !pos + 1 in
    let b = Bytes.create len in
    Bigstring.To_bytes.unsafe_blit ~src:t.buffer ~dst:b ~src_pos:!pos ~dst_pos:0 ~len;
    Bytes.unsafe_to_string ~no_mutation_while_string_reachable:b
  ;;

  let[@inline always] index t ch =
    let idx = Bigstring.unsafe_find t.buffer ch ~pos:t.pos ~len:(length t) in
    if idx < 0 then -1 else idx - t.pos
  ;;

  let[@inline always] consume_eol t =
    if length t < 2 then raise_notrace Partial;
    if Char.(
         Bigstring.get t.buffer t.pos = '\r' && Bigstring.get t.buffer (t.pos + 1) = '\n')
    then unsafe_advance t 2
    else raise_notrace (Fail (Error.of_string "Expected EOL"))
  ;;

  let parse_header tchar_map source =
    let pos = index source ':' in
    if pos = -1
    then raise_notrace Partial
    else if pos = 0
    then raise_notrace (Fail (Error.of_string "Invalid header: Empty header key"));
    for idx = 0 to pos - 1 do
      if not (Array.unsafe_get tchar_map (Char.to_int (unsafe_get source idx)))
      then raise_notrace (Fail (Error.of_string "Invalid Header Key"))
    done;
    let key = to_string source ~pos:0 ~len:pos in
    unsafe_advance source (pos + 1);
    let pos = index source '\r' in
    if pos = -1 then raise_notrace Partial;
    let v = to_string_trim source ~pos:0 ~len:pos in
    unsafe_advance source pos;
    key, v
  ;;
end

let[@inline always] ( .![] ) source idx = Source.unsafe_get source idx
let invalid_method = Fail (Error.of_string "Invalid Method")

let meth source =
  let pos = Source.index source ' ' in
  if pos = -1 then raise_notrace Partial;
  let meth =
    match pos with
    | 3 ->
      (match source.![0], source.![1], source.![2] with
       | 'G', 'E', 'T' -> `GET
       | 'P', 'U', 'T' -> `PUT
       | _ -> raise_notrace invalid_method)
    | 4 ->
      (match source.![0], source.![1], source.![2], source.![3] with
       | 'H', 'E', 'A', 'D' -> `HEAD
       | 'P', 'O', 'S', 'T' -> `POST
       | _ -> raise_notrace invalid_method)
    | 5 ->
      (match source.![0], source.![1], source.![2], source.![3], source.![4] with
       | 'P', 'A', 'T', 'C', 'H' -> `PATCH
       | 'T', 'R', 'A', 'C', 'E' -> `TRACE
       | _ -> raise_notrace invalid_method)
    | 6 ->
      (match
         source.![0], source.![1], source.![2], source.![3], source.![4], source.![5]
       with
       | 'D', 'E', 'L', 'E', 'T', 'E' -> `DELETE
       | _ -> raise_notrace invalid_method)
    | 7 ->
      (match
         ( source.![0]
         , source.![1]
         , source.![2]
         , source.![3]
         , source.![4]
         , source.![5]
         , source.![6] )
       with
       | 'C', 'O', 'N', 'N', 'E', 'C', 'T' -> `CONNECT
       | 'O', 'P', 'T', 'I', 'O', 'N', 'S' -> `OPTIONS
       | _ -> raise_notrace invalid_method)
    | _ -> raise_notrace invalid_method
  in
  Source.unsafe_advance source (pos + 1);
  meth
;;

let rec headers source =
  if (not (Source.is_empty source)) && Char.(Source.unsafe_get source 0 = '\r')
  then (
    Source.consume_eol source;
    [])
  else (
    let header = Source.parse_header tchar_map source in
    Source.consume_eol source;
    header :: headers source)
;;

let chunk_length source =
  let length = ref 0 in
  let stop = ref false in
  let state = ref `Ok in
  let count = ref 0 in
  let processing_chunk = ref true in
  let in_chunk_extension = ref false in
  while not !stop do
    if Source.is_empty source
    then (
      stop := true;
      state := `Partial)
    else if !count = 16 && not !in_chunk_extension
    then (
      stop := true;
      state := `Chunk_too_big)
    else (
      let ch = Source.unsafe_get source 0 in
      Source.unsafe_advance source 1;
      incr count;
      match ch with
      | '0' .. '9' as ch when !processing_chunk ->
        let curr = Char.to_int ch - Char.to_int '0' in
        length := (!length lsl 4) lor curr
      | 'a' .. 'f' as ch when !processing_chunk ->
        let curr = Char.to_int ch - Char.to_int 'a' + 10 in
        length := (!length lsl 4) lor curr
      | 'A' .. 'F' as ch when !processing_chunk ->
        let curr = Char.to_int ch - Char.to_int 'A' + 10 in
        length := (!length lsl 4) lor curr
      | ';' when not !in_chunk_extension ->
        in_chunk_extension := true;
        processing_chunk := false
      | ('\t' | ' ') when !processing_chunk -> processing_chunk := false
      | ('\t' | ' ') when (not !in_chunk_extension) && not !processing_chunk -> ()
      | '\r' ->
        if Source.is_empty source
        then (
          stop := true;
          state := `Partial)
        else if Char.(Source.unsafe_get source 0 = '\n')
        then (
          Source.unsafe_advance source 1;
          stop := true)
        else (
          stop := true;
          state := `Expected_newline)
      | _ when !in_chunk_extension ->
        (* Chunk extensions aren't very common, see:
           https://tools.ietf.org/html/rfc7230#section-4.1.1 Chunk extensions aren't
           pre-defined, and they are specific to invidividual connections. In the future
           we might surface these to the user somehow, but for now we will ignore any
           extensions. TODO: Should there be any limit on the size of chunk extensions we
           parse? We might want to error if a request contains really large chunk
           extensions. *)
        ()
      | ch ->
        stop := true;
        state := `Invalid_char ch)
  done;
  match !state with
  | `Ok -> !length
  | `Partial -> raise_notrace Partial
  | `Expected_newline -> raise_notrace (Fail (Error.of_string "Expected_newline"))
  | `Chunk_too_big -> raise_notrace (Fail (Error.of_string "Chunk size is too large"))
  | `Invalid_char ch ->
    raise_notrace (Fail (Error.create "Invalid chunk_length character" ch sexp_of_char))
;;

let version source =
  if Source.length source < 8 then raise_notrace Partial;
  if Char.equal source.![0] 'H'
     && Char.equal source.![1] 'T'
     && Char.equal source.![2] 'T'
     && Char.equal source.![3] 'P'
     && Char.equal source.![4] '/'
     && Char.equal source.![5] '1'
     && Char.equal source.![6] '.'
     && Char.equal source.![7] '1'
  then (
    Source.unsafe_advance source 8;
    Source.consume_eol source;
    Version.Http_1_1)
  else raise_notrace (Fail (Error.of_string "Invalid HTTP Version"))
;;

let token source =
  let pos = Source.index source ' ' in
  if pos = -1 then raise_notrace Partial;
  let res = Source.to_string source ~pos:0 ~len:pos in
  Source.unsafe_advance source (pos + 1);
  res
;;

let request source =
  let meth = meth source in
  let path = token source in
  let version = version source in
  let headers = Headers.of_rev_list (headers source) in
  Request.create ~version ~headers meth path
;;

let take len source =
  let available = Source.length source in
  let to_consume = min len available in
  if to_consume = 0 then raise_notrace Partial;
  let payload = Source.to_iovec source ~pos:0 ~len:to_consume in
  Source.unsafe_advance source to_consume;
  payload
;;

type chunk_kind =
  | Start_chunk
  | Continue_chunk of int

type chunk_parser_result =
  | Chunk_complete of Bigstring.t Core_unix.IOVec.t
  | Done
  | Partial_chunk of Bigstring.t Core_unix.IOVec.t * int

let chunk chunk_kind source =
  match chunk_kind with
  | Start_chunk ->
    let chunk_length = chunk_length source in
    if chunk_length = 0
    then (
      Source.consume_eol source;
      Done)
    else (
      let current_chunk = take chunk_length source in
      let current_chunk_length = current_chunk.len in
      if current_chunk_length = chunk_length
      then (
        Source.consume_eol source;
        Chunk_complete current_chunk)
      else Partial_chunk (current_chunk, chunk_length - current_chunk_length))
  | Continue_chunk len ->
    let chunk = take len source in
    let current_chunk_length = chunk.len in
    if current_chunk_length = len
    then (
      Source.consume_eol source;
      Chunk_complete chunk)
    else Partial_chunk (chunk, len - current_chunk_length)
;;

type error =
  | Partial
  | Fail of Error.t

let run_parser ?(pos = 0) ?len buf p =
  let total_length = Bigstring.length buf in
  let len =
    match len with
    | Some v -> v
    | None -> total_length - pos
  in
  Ordered_collection_common.check_pos_len_exn ~pos ~len ~total_length;
  let source = Source.{ buffer = buf; pos; upper_bound = pos + len } in
  match p source with
  | exception Partial -> Error Partial
  | exception Fail m -> Error (Fail m)
  | v ->
    let consumed = source.pos - pos in
    Ok (v, consumed)
;;

let parse_request ?pos ?len buf = run_parser ?pos ?len buf request
let parse_chunk_length ?pos ?len buf = run_parser ?pos ?len buf chunk_length
let parse_chunk ?pos ?len buf chunk_kind = run_parser ?pos ?len buf (chunk chunk_kind)

module Private = struct
  let parse_method payload = run_parser (Bigstring.of_string payload) meth
end
OCaml

Innovation. Community. Security.