An OCaml webserver, but the allocating version (vs httpz which doesnt)
at main 325 lines 13 kB view raw
1(* date.ml - HTTP-date parsing and formatting per RFC 7231 Section 7.1.1.1 *) 2 3open Base 4 5type status = 6 | Valid 7 | Invalid 8 9(* Day and month names for parsing and formatting *) 10let day_names = [| "Sun"; "Mon"; "Tue"; "Wed"; "Thu"; "Fri"; "Sat" |] 11let month_names = [| "Jan"; "Feb"; "Mar"; "Apr"; "May"; "Jun"; "Jul"; "Aug"; "Sep"; "Oct"; "Nov"; "Dec" |] 12 13(* Parse 2-digit number at position, returns (value, valid) *) 14let parse_2digit buf pos = 15 let c0 = Base_bigstring.unsafe_get buf pos in 16 let c1 = Base_bigstring.unsafe_get buf (pos + 1) in 17 if Char.is_digit c0 && Char.is_digit c1 then 18 ((Char.to_int c0 - 48) * 10 + (Char.to_int c1 - 48), true) 19 else 20 (0, false) 21;; 22 23(* Parse 4-digit year at position, returns (value, valid) *) 24let parse_4digit buf pos = 25 let c0 = Base_bigstring.unsafe_get buf pos in 26 let c1 = Base_bigstring.unsafe_get buf (pos + 1) in 27 let c2 = Base_bigstring.unsafe_get buf (pos + 2) in 28 let c3 = Base_bigstring.unsafe_get buf (pos + 3) in 29 if Char.is_digit c0 && Char.is_digit c1 && Char.is_digit c2 && Char.is_digit c3 then 30 ((Char.to_int c0 - 48) * 1000 + (Char.to_int c1 - 48) * 100 + 31 (Char.to_int c2 - 48) * 10 + (Char.to_int c3 - 48), true) 32 else 33 (0, false) 34;; 35 36(* Parse 1 or 2 digit day, returns (day, next_pos, valid) *) 37let parse_day buf pos len = 38 if pos >= len then (0, pos, false) 39 else 40 let c0 = Base_bigstring.unsafe_get buf pos in 41 if Char.equal c0 ' ' && pos + 1 < len then 42 (* Space-padded single digit *) 43 let c1 = Base_bigstring.unsafe_get buf (pos + 1) in 44 if Char.is_digit c1 then (Char.to_int c1 - 48, pos + 2, true) 45 else (0, pos, false) 46 else if Char.is_digit c0 && pos + 1 < len then 47 let c1 = Base_bigstring.unsafe_get buf (pos + 1) in 48 if Char.is_digit c1 then 49 ((Char.to_int c0 - 48) * 10 + (Char.to_int c1 - 48), pos + 2, true) 50 else 51 (Char.to_int c0 - 48, pos + 1, true) 52 else 53 (0, pos, false) 54;; 55 56(* Parse 3-letter month abbreviation, returns 0-11 or -1 *) 57let parse_month buf pos = 58 let c0 = Base_bigstring.unsafe_get buf pos in 59 let c1 = Base_bigstring.unsafe_get buf (pos + 1) in 60 let c2 = Base_bigstring.unsafe_get buf (pos + 2) in 61 match (c0, c1, c2) with 62 | ('J', 'a', 'n') -> 0 63 | ('F', 'e', 'b') -> 1 64 | ('M', 'a', 'r') -> 2 65 | ('A', 'p', 'r') -> 3 66 | ('M', 'a', 'y') -> 4 67 | ('J', 'u', 'n') -> 5 68 | ('J', 'u', 'l') -> 6 69 | ('A', 'u', 'g') -> 7 70 | ('S', 'e', 'p') -> 8 71 | ('O', 'c', 't') -> 9 72 | ('N', 'o', 'v') -> 10 73 | ('D', 'e', 'c') -> 11 74 | _ -> -1 75;; 76 77(* Parse time HH:MM:SS at position, returns (hour, minute, second, valid) *) 78let parse_time buf pos = 79 let (hour, h_valid) = parse_2digit buf pos in 80 if not h_valid then (0, 0, 0, false) 81 else if not (Char.equal (Base_bigstring.unsafe_get buf (pos + 2)) ':') then (0, 0, 0, false) 82 else 83 let (minute, m_valid) = parse_2digit buf (pos + 3) in 84 if not m_valid then (0, 0, 0, false) 85 else if not (Char.equal (Base_bigstring.unsafe_get buf (pos + 5)) ':') then (0, 0, 0, false) 86 else 87 let (second, s_valid) = parse_2digit buf (pos + 6) in 88 if not s_valid then (0, 0, 0, false) 89 else if hour > 23 || minute > 59 || second > 60 then (0, 0, 0, false) (* 60 for leap second *) 90 else (hour, minute, second, true) 91;; 92 93(* Days in each month (non-leap year) *) 94let days_in_month = [| 31; 28; 31; 30; 31; 30; 31; 31; 30; 31; 30; 31 |] 95 96(* Check if year is leap year *) 97let is_leap_year year = 98 (year % 4 = 0 && year % 100 <> 0) || (year % 400 = 0) 99;; 100 101(* Convert date components to Unix timestamp, returns (timestamp, valid) *) 102let to_timestamp ~year ~month ~day ~hour ~minute ~second = 103 (* Validate ranges *) 104 if year < 1970 || month < 0 || month > 11 then (0.0, false) 105 else 106 let max_day = 107 if month = 1 && is_leap_year year then 29 108 else days_in_month.(month) 109 in 110 if day < 1 || day > max_day then (0.0, false) 111 else 112 (* Calculate days since epoch *) 113 let days = ref 0 in 114 (* Add days for complete years *) 115 for y = 1970 to year - 1 do 116 days := !days + (if is_leap_year y then 366 else 365) 117 done; 118 (* Add days for complete months in current year *) 119 for m = 0 to month - 1 do 120 days := !days + days_in_month.(m); 121 if m = 1 && is_leap_year year then days := !days + 1 122 done; 123 (* Add days in current month *) 124 days := !days + (day - 1); 125 (* Convert to seconds and add time *) 126 let timestamp = 127 Float.of_int !days *. 86400.0 +. 128 Float.of_int hour *. 3600.0 +. 129 Float.of_int minute *. 60.0 +. 130 Float.of_int second 131 in 132 (timestamp, true) 133;; 134 135let invalid_result = (0.0, false) 136 137(* Parse IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT *) 138let parse_imf_fixdate buf off len = 139 (* Minimum length: "Sun, 06 Nov 1994 08:49:37 GMT" = 29 chars *) 140 if len < 29 then invalid_result 141 else 142 (* Skip day name - find comma *) 143 let comma_pos = ref off in 144 while !comma_pos < off + 4 && not (Char.equal (Base_bigstring.unsafe_get buf !comma_pos) ',') do 145 Int.incr comma_pos 146 done; 147 if !comma_pos >= off + len || not (Char.equal (Base_bigstring.unsafe_get buf !comma_pos) ',') then invalid_result 148 else if not (Char.equal (Base_bigstring.unsafe_get buf (!comma_pos + 1)) ' ') then invalid_result 149 else 150 let day_pos = !comma_pos + 2 in 151 let (day, day_valid) = parse_2digit buf day_pos in 152 if not day_valid then invalid_result 153 else if not (Char.equal (Base_bigstring.unsafe_get buf (day_pos + 2)) ' ') then invalid_result 154 else 155 let month = parse_month buf (day_pos + 3) in 156 if month < 0 then invalid_result 157 else if not (Char.equal (Base_bigstring.unsafe_get buf (day_pos + 6)) ' ') then invalid_result 158 else 159 let (year, year_valid) = parse_4digit buf (day_pos + 7) in 160 if not year_valid then invalid_result 161 else if not (Char.equal (Base_bigstring.unsafe_get buf (day_pos + 11)) ' ') then invalid_result 162 else 163 let (hour, minute, second, time_valid) = parse_time buf (day_pos + 12) in 164 if not time_valid then invalid_result 165 else 166 (* Check for " GMT" at end *) 167 let gmt_pos = day_pos + 20 in 168 if gmt_pos + 4 > off + len then invalid_result 169 else if not (Char.equal (Base_bigstring.unsafe_get buf gmt_pos) ' ') then invalid_result 170 else if not (Char.equal (Base_bigstring.unsafe_get buf (gmt_pos + 1)) 'G') then invalid_result 171 else if not (Char.equal (Base_bigstring.unsafe_get buf (gmt_pos + 2)) 'M') then invalid_result 172 else if not (Char.equal (Base_bigstring.unsafe_get buf (gmt_pos + 3)) 'T') then invalid_result 173 else to_timestamp ~year ~month ~day ~hour ~minute ~second 174;; 175 176(* Parse RFC 850 date: Sunday, 06-Nov-94 08:49:37 GMT *) 177let parse_rfc850 buf off len = 178 (* Find comma after full day name *) 179 let comma_pos = ref off in 180 while !comma_pos < off + 10 && not (Char.equal (Base_bigstring.unsafe_get buf !comma_pos) ',') do 181 Int.incr comma_pos 182 done; 183 if !comma_pos >= off + len || not (Char.equal (Base_bigstring.unsafe_get buf !comma_pos) ',') then invalid_result 184 else if not (Char.equal (Base_bigstring.unsafe_get buf (!comma_pos + 1)) ' ') then invalid_result 185 else 186 let pos = !comma_pos + 2 in 187 let (day, day_valid) = parse_2digit buf pos in 188 if not day_valid then invalid_result 189 else if not (Char.equal (Base_bigstring.unsafe_get buf (pos + 2)) '-') then invalid_result 190 else 191 let month = parse_month buf (pos + 3) in 192 if month < 0 then invalid_result 193 else if not (Char.equal (Base_bigstring.unsafe_get buf (pos + 6)) '-') then invalid_result 194 else 195 let (year2, year2_valid) = parse_2digit buf (pos + 7) in 196 if not year2_valid then invalid_result 197 else 198 (* RFC 850 uses 2-digit year. Interpret 00-99 as 2000-2099 for dates >= 70, 199 and 1970-1999 for dates < 70. Modern interpretation varies. *) 200 let year = if year2 >= 70 then 1900 + year2 else 2000 + year2 in 201 if not (Char.equal (Base_bigstring.unsafe_get buf (pos + 9)) ' ') then invalid_result 202 else 203 let (hour, minute, second, time_valid) = parse_time buf (pos + 10) in 204 if not time_valid then invalid_result 205 else 206 (* Check for " GMT" *) 207 let gmt_pos = pos + 18 in 208 if gmt_pos + 4 > off + len then invalid_result 209 else if not (Char.equal (Base_bigstring.unsafe_get buf gmt_pos) ' ') then invalid_result 210 else if not (Char.equal (Base_bigstring.unsafe_get buf (gmt_pos + 1)) 'G') then invalid_result 211 else if not (Char.equal (Base_bigstring.unsafe_get buf (gmt_pos + 2)) 'M') then invalid_result 212 else if not (Char.equal (Base_bigstring.unsafe_get buf (gmt_pos + 3)) 'T') then invalid_result 213 else to_timestamp ~year ~month ~day ~hour ~minute ~second 214;; 215 216(* Parse asctime format: Sun Nov 6 08:49:37 1994 *) 217let parse_asctime buf off len = 218 (* Minimum length: "Sun Nov 6 08:49:37 1994" = 24 chars *) 219 if len < 24 then invalid_result 220 (* Skip 3-char day name and space *) 221 else if not (Char.equal (Base_bigstring.unsafe_get buf (off + 3)) ' ') then invalid_result 222 else 223 let month = parse_month buf (off + 4) in 224 if month < 0 then invalid_result 225 else if not (Char.equal (Base_bigstring.unsafe_get buf (off + 7)) ' ') then invalid_result 226 else 227 let (day, next_pos, day_valid) = parse_day buf (off + 8) len in 228 if not day_valid then invalid_result 229 else if not (Char.equal (Base_bigstring.unsafe_get buf next_pos) ' ') then invalid_result 230 else 231 let (hour, minute, second, time_valid) = parse_time buf (next_pos + 1) in 232 if not time_valid then invalid_result 233 else 234 let year_pos = next_pos + 9 in 235 if not (Char.equal (Base_bigstring.unsafe_get buf year_pos) ' ') then invalid_result 236 else 237 let (year, year_valid) = parse_4digit buf (year_pos + 1) in 238 if not year_valid then invalid_result 239 else to_timestamp ~year ~month ~day ~hour ~minute ~second 240;; 241 242(* Main parse function - tries all three formats *) 243let parse buf (sp : Span.t) = 244 let off = Span.off sp in 245 let len = Span.len sp in 246 if len < 24 then (Invalid, 0.0) 247 else 248 (* Check for comma to distinguish IMF-fixdate/RFC850 from asctime *) 249 let c4 = Base_bigstring.unsafe_get buf (off + 3) in 250 let (ts, valid) = 251 if Char.equal c4 ',' then 252 (* IMF-fixdate: short day name + comma *) 253 parse_imf_fixdate buf off len 254 else if Char.equal c4 ' ' then 255 (* asctime: short day name + space *) 256 parse_asctime buf off len 257 else 258 (* RFC 850: full day name, look for comma *) 259 parse_rfc850 buf off len 260 in 261 if valid then (Valid, ts) else (Invalid, 0.0) 262;; 263 264(* Format timestamp as IMF-fixdate *) 265let format timestamp = 266 (* Use Unix module to break down timestamp *) 267 let tm = Unix.gmtime timestamp in 268 Stdlib.Printf.sprintf "%s, %02d %s %04d %02d:%02d:%02d GMT" 269 day_names.(tm.Unix.tm_wday) 270 tm.Unix.tm_mday 271 month_names.(tm.Unix.tm_mon) 272 (tm.Unix.tm_year + 1900) 273 tm.Unix.tm_hour 274 tm.Unix.tm_min 275 tm.Unix.tm_sec 276;; 277 278(* Write HTTP-date at offset without header name *) 279let write_http_date dst ~off timestamp = 280 let tm = Unix.gmtime timestamp in 281 let off = Buf_write.string dst ~off day_names.(tm.Unix.tm_wday) in 282 let off = Buf_write.string dst ~off ", " in 283 let off = Buf_write.digit2 dst ~off tm.Unix.tm_mday in 284 let off = Buf_write.char dst ~off ' ' in 285 let off = Buf_write.string dst ~off month_names.(tm.Unix.tm_mon) in 286 let off = Buf_write.char dst ~off ' ' in 287 let off = Buf_write.digit4 dst ~off (tm.Unix.tm_year + 1900) in 288 let off = Buf_write.char dst ~off ' ' in 289 let off = Buf_write.digit2 dst ~off tm.Unix.tm_hour in 290 let off = Buf_write.char dst ~off ':' in 291 let off = Buf_write.digit2 dst ~off tm.Unix.tm_min in 292 let off = Buf_write.char dst ~off ':' in 293 let off = Buf_write.digit2 dst ~off tm.Unix.tm_sec in 294 Buf_write.string dst ~off " GMT" 295;; 296 297let write_date_header dst ~off timestamp = 298 let off = Buf_write.string dst ~off "Date: " in 299 let off = write_http_date dst ~off timestamp in 300 Buf_write.crlf dst ~off 301;; 302 303let write_last_modified dst ~off timestamp = 304 let off = Buf_write.string dst ~off "Last-Modified: " in 305 let off = write_http_date dst ~off timestamp in 306 Buf_write.crlf dst ~off 307;; 308 309let write_expires dst ~off timestamp = 310 let off = Buf_write.string dst ~off "Expires: " in 311 let off = write_http_date dst ~off timestamp in 312 Buf_write.crlf dst ~off 313;; 314 315(* Comparison helpers *) 316let is_modified_since ~last_modified ~if_modified_since = 317 (* Resource is modified if last_modified > if_modified_since 318 Note: HTTP dates have 1-second resolution, so we use > not >= *) 319 Float.(last_modified > if_modified_since) 320;; 321 322let is_unmodified_since ~last_modified ~if_unmodified_since = 323 (* Resource is unmodified if last_modified <= if_unmodified_since *) 324 Float.(last_modified <= if_unmodified_since) 325;;