forked from
anil.recoil.org/ocaml-requests
A batteries included HTTP/1.1 client in OCaml
1(*---------------------------------------------------------------------------
2 Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved.
3 SPDX-License-Identifier: ISC
4 ---------------------------------------------------------------------------*)
5
6(** HTTP-date parsing per RFC 9110 Section 5.6.7 *)
7
8let src = Logs.Src.create "requests.http_date" ~doc:"HTTP Date Parsing"
9
10module Log = (val Logs.src_log src : Logs.LOG)
11
12(** Parse HTTP-date (RFC 9110 Section 5.6.7) to Ptime.t *)
13let parse s =
14 (* HTTP-date format: "Sun, 06 Nov 1994 08:49:37 GMT" (RFC 1123) *)
15 (* Also supports obsolete formats per RFC 9110 *)
16 let s = String.trim s in
17
18 (* Helper to parse month name *)
19 let parse_month month_str =
20 match String.lowercase_ascii month_str with
21 | "jan" -> 1
22 | "feb" -> 2
23 | "mar" -> 3
24 | "apr" -> 4
25 | "may" -> 5
26 | "jun" -> 6
27 | "jul" -> 7
28 | "aug" -> 8
29 | "sep" -> 9
30 | "oct" -> 10
31 | "nov" -> 11
32 | "dec" -> 12
33 | _ -> failwith "invalid month"
34 in
35
36 (* Validate time components per RFC 9110 Section 5.6.7 ABNF:
37 hour = 2DIGIT (00-23), minute = 2DIGIT (00-59), second = 2DIGIT (00-59).
38 Note: RFC 9110 inherits the 00-60 range from RFC 5234 to allow leap
39 seconds, but HTTP servers MUST NOT generate them and recipients SHOULD
40 treat second=60 as invalid. We reject second=60 for robustness. *)
41 let validate_time hour min sec =
42 hour >= 0 && hour <= 23 && min >= 0 && min <= 59 && sec >= 0 && sec <= 59
43 in
44
45 let make_datetime year month day hour min sec =
46 if validate_time hour min sec then
47 Ptime.of_date_time ((year, month, day), ((hour, min, sec), 0))
48 else None
49 in
50
51 (* Try different date formats in order of preference *)
52 let parsers =
53 [
54 (* RFC 1123 format: "Sun, 06 Nov 1994 08:49:37 GMT" *)
55 (fun () ->
56 Scanf.sscanf s "%_s %d %s %d %d:%d:%d GMT"
57 (fun day month_str year hour min sec ->
58 let month = parse_month month_str in
59 make_datetime year month day hour min sec));
60 (* RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT" *)
61 (fun () ->
62 Scanf.sscanf s "%_s %d-%s@-%d %d:%d:%d GMT"
63 (fun day month_str year2 hour min sec ->
64 let year = if year2 >= 70 then 1900 + year2 else 2000 + year2 in
65 let month = parse_month month_str in
66 make_datetime year month day hour min sec));
67 (* ANSI C asctime() format: "Sun Nov 6 08:49:37 1994" *)
68 (fun () ->
69 Scanf.sscanf s "%_s %s %d %d:%d:%d %d"
70 (fun month_str day hour min sec year ->
71 let month = parse_month month_str in
72 make_datetime year month day hour min sec));
73 ]
74 in
75
76 (* Try each parser until one succeeds *)
77 List.find_map
78 (fun parser ->
79 try parser ()
80 with Scanf.Scan_failure _ | Failure _ | End_of_file -> None)
81 parsers