Commit | Line | Data |
---|---|---|
7f918cf1 CE |
1 | (* Copyright (C) 2009 Matthew Fluet. |
2 | * Copyright (C) 1999-2006 Henry Cejtin, Matthew Fluet, Suresh | |
3 | * Jagannathan, and Stephen Weeks. | |
4 | * | |
5 | * MLton is released under a BSD-style license. | |
6 | * See the file MLton-LICENSE for details. | |
7 | *) | |
8 | ||
9 | structure Http: HTTP = | |
10 | struct | |
11 | ||
12 | structure Regexp = | |
13 | struct | |
14 | open Regexp | |
15 | ||
16 | val CHAR = ascii | |
17 | val UPALPHA = isChar Char.isUpper | |
18 | val LOALPHA = isChar Char.isLower | |
19 | val ALPHA = isChar Char.isAlpha | |
20 | val DIGIT = isChar Char.isDigit | |
21 | val CTL = isChar Char.isCntrl | |
22 | val CR = char #"\r" | |
23 | val LF = char #"\n" | |
24 | val SP = char #" " | |
25 | val HT = char #"\t" | |
26 | val CRLF = string "\r\n" | |
27 | (* #"\n" is not in the spec for CRLF, but Netscape generates it. *) | |
28 | val CRLF = or [CRLF, char #"\n"] | |
29 | val LWS = seq [optional CRLF, oneOrMore (or [SP, HT])] | |
30 | val TEXT = isNotChar Char.isCntrl | |
31 | val HEX = isChar Char.isHexDigit | |
32 | val separatorChars = "()<>@,;:\\\"/ []?= {} \t" | |
33 | val separators = oneOf separatorChars | |
34 | val token = | |
35 | oneOrMore (isChar | |
36 | (Char.memoize | |
37 | (fn c => | |
38 | Char.isAscii c | |
39 | andalso not (Char.isCntrl c) | |
40 | andalso not (String.contains (separatorChars, c))))) | |
41 | val dquote = char Char.dquote | |
42 | val qdtext = isChar (fn c => | |
43 | not (Char.isCntrl c) | |
44 | andalso not (c = Char.dquote)) | |
45 | val quotedpair = seq [char #"\\", CHAR] | |
46 | val quotedstring = | |
47 | seq [dquote, star (or [qdtext, quotedpair]), dquote] | |
48 | val ctext = | |
49 | isChar (fn c => | |
50 | not (Char.isCntrl c) | |
51 | andalso not (c = #"(") | |
52 | andalso not (c = #")")) | |
53 | (* val comment = | |
54 | * seq [char #"(", | |
55 | * star (or [ctext, quoted-pair, comment]), | |
56 | * char #")"] | |
57 | *) | |
58 | val major' = Save.new () | |
59 | val minor' = Save.new () | |
60 | val version = | |
61 | seq [string "HTTP/", | |
62 | save (oneOrMore DIGIT, major'), | |
63 | char #".", | |
64 | save (oneOrMore DIGIT, minor')] | |
65 | val fieldname' = Save.new () | |
66 | val fieldname = | |
67 | (* fieldname should just be token, but the stupid Microsoft server | |
68 | * includes spaces in its "Content Location" field, so we need to accept | |
69 | * more. The easies thing is just to take anything but a ":". | |
70 | *) | |
71 | (* save (token, fieldname') *) | |
72 | save (star (notChar #":"), fieldname') | |
73 | val fieldcontent = | |
74 | (* fieldcontent should just be TEXT, but nytimes stores control | |
75 | * characters in cookies, and thus, need to allow more. | |
76 | *) | |
77 | (*TEXT *) | |
78 | isChar (fn c => c >= #" ") | |
79 | val fieldvalue' = Save.new () | |
80 | val fieldvalue = star (or [fieldcontent, LWS]) | |
81 | val messageheader = | |
82 | Promise.lazy | |
83 | (fn () => | |
84 | compileDFA (seq [fieldname, char #":", | |
85 | save (optional fieldvalue, fieldvalue'), | |
86 | CRLF])) | |
87 | val method' = Save.new () | |
88 | val method = save (token, method') | |
89 | val star' = Save.new () | |
90 | val absoluteUrl' = Save.new () | |
91 | val absPath' = Save.new () | |
92 | val authority' = Save.new () | |
93 | val query' = Save.new () | |
94 | val requestUrl = | |
95 | let open Url.Regexp | |
96 | in or [save (char #"*", star'), | |
97 | save (absoluteUrl, absoluteUrl'), | |
98 | seq [save (absPath, absPath'), | |
99 | optional (seq [char #"?", save (query, query')])], | |
100 | save (authority, authority')] | |
101 | end | |
102 | val requestLine = | |
103 | Promise.lazy | |
104 | (fn () => | |
105 | compileDFA (seq [method, SP, requestUrl, SP, version, CRLF])) | |
106 | val contentLength = | |
107 | Promise.lazy (fn () => compileDFA (oneOrMore DIGIT)) | |
108 | val status' = Save.new () | |
109 | val status = save (seq [DIGIT, DIGIT, DIGIT], status') | |
110 | val reason = | |
111 | star (isChar (fn c => | |
112 | Char.isPrint c andalso c <> #"\r" andalso c <> #"\n")) | |
113 | val responseLine = | |
114 | Promise.lazy | |
115 | (fn () => compileDFA (seq [version, SP, status, SP, reason, CRLF])) | |
116 | end | |
117 | ||
118 | structure Method = | |
119 | struct | |
120 | datatype t = | |
121 | Connect | |
122 | | Delete | |
123 | | Extension of string | |
124 | | Get | |
125 | | Head | |
126 | | Options | |
127 | | Post | |
128 | | Put | |
129 | | Trace | |
130 | ||
131 | val map = | |
132 | [(Connect, "CONNECT"), | |
133 | (Delete, "DELETE"), | |
134 | (Get, "GET"), | |
135 | (Head, "HEAD"), | |
136 | (Options, "OPTIONS"), | |
137 | (Post, "POST"), | |
138 | (Put, "PUT"), | |
139 | (Trace, "TRACE")] | |
140 | ||
141 | fun fromString s = | |
142 | case List.peek (map, fn (_, s') => s = s') of | |
143 | NONE => Extension s | |
144 | | SOME (h, _) => h | |
145 | ||
146 | fun toString h = | |
147 | case h of | |
148 | Extension s => s | |
149 | | _ => #2 (valOf (List.peek (map, fn (h', _) => h = h'))) | |
150 | ||
151 | val layout = Layout.str o toString | |
152 | end | |
153 | ||
154 | structure Version = | |
155 | struct | |
156 | datatype t = T of {major: int, | |
157 | minor: int} | |
158 | ||
159 | fun toString (T {major, minor}) = | |
160 | concat ["HTTP/", | |
161 | Int.toString major, | |
162 | ".", | |
163 | Int.toString minor] | |
164 | ||
165 | val layout = Layout.str o toString | |
166 | ||
167 | val v10 = T {major = 1, minor = 0} | |
168 | val v11 = T {major = 1, minor = 1} | |
169 | ||
170 | fun extract m = | |
171 | T (let | |
172 | open Regexp | |
173 | fun int s = valOf (Int.fromString (Substring.toString | |
174 | (Match.lookup (m, s)))) | |
175 | in {minor = int minor', | |
176 | major = int major'} | |
177 | end) | |
178 | end | |
179 | ||
180 | structure RequestUrl = | |
181 | struct | |
182 | structure Path = Url.Path | |
183 | datatype t = | |
184 | Star | |
185 | | Url of Url.t | |
186 | | Path of {path: Path.t, | |
187 | query: string option} | |
188 | | Authority of string | |
189 | ||
190 | val toString = | |
191 | fn Star => "*" | |
192 | | Url url => Url.toString url | |
193 | | Path {path, query} => | |
194 | concat [Path.toString path, | |
195 | case query of | |
196 | NONE => "" | |
197 | | SOME q => concat ["?", if !Url.escapeQuery | |
198 | then Url.escape q | |
199 | else q]] | |
200 | | Authority s => s | |
201 | ||
202 | val layout = Layout.str o toString | |
203 | end | |
204 | ||
205 | exception ParseError | |
206 | ||
207 | structure Header = | |
208 | struct | |
209 | datatype t = | |
210 | Accept of string | |
211 | | AcceptCharset of string | |
212 | | AcceptEncoding of string | |
213 | | AcceptLanguage of string | |
214 | | AcceptRanges of string | |
215 | | Age of string | |
216 | | Allow of string | |
217 | | Authorization of string | |
218 | | CacheControl of string | |
219 | | Connection of string | |
220 | | ContentEncoding of string | |
221 | | ContentLanguage of string | |
222 | | ContentLength of int | |
223 | | ContentLocation of string | |
224 | | ContentMD5 of string | |
225 | | ContentRange of string | |
226 | | ContentType of string | |
227 | | Cookie of string | |
228 | | Date of string | |
229 | | ETag of string | |
230 | | Expect of string | |
231 | | Expires of string | |
232 | | Extension of {name: string, value: string} | |
233 | | From of string | |
234 | | Host of string | |
235 | | IfMatch of string | |
236 | | LastModified of string | |
237 | | Location of string | |
238 | | Pragma of string | |
239 | | ProxyAuthenticate of string | |
240 | | ProxyConnection of string | |
241 | | Referer of string | |
242 | | RetryAfter of string | |
243 | | Server of string | |
244 | | SetCookie of string | |
245 | | Trailer of string | |
246 | | TransferEncoding of string | |
247 | | Upgrade of string | |
248 | | UserAgent of string | |
249 | | Vary of string | |
250 | | Via of string | |
251 | | WWWAuthenticate of string | |
252 | | Warning of string | |
253 | ||
254 | val toString = | |
255 | fn Accept s => concat ["Accept: ", s] | |
256 | | AcceptCharset s => concat ["Accept-Charset: ", s] | |
257 | | AcceptEncoding s => concat ["Accept-Encoding: ", s] | |
258 | | AcceptLanguage s => concat ["Accept-Language: ", s] | |
259 | | AcceptRanges s => concat ["Accept-Ranges: ", s] | |
260 | | Age s => concat ["Age: ", s] | |
261 | | Allow s => concat ["Allow: ", s] | |
262 | | Authorization s => concat ["Authorization: Basic ", Base64.encode s] | |
263 | | CacheControl s => concat ["Cache-Control: ", s] | |
264 | | Connection s => concat ["Connection: ", s] | |
265 | | ContentEncoding s => concat ["Content-Encoding: ", s] | |
266 | | ContentLanguage s => concat ["Content-Language: ", s] | |
267 | | ContentLength s => concat ["Content-Length: ", Int.toString s] | |
268 | | ContentLocation s => concat ["Content-Location: ", s] | |
269 | | ContentMD5 s => concat ["Content-MD5: ", s] | |
270 | | ContentRange s => concat ["Content-Range: ", s] | |
271 | | ContentType s => concat ["Content-Type: ", s] | |
272 | | Cookie s => concat ["Cookie: ", s] | |
273 | | Date s => concat ["Date: ", s] | |
274 | | ETag s => concat ["Etag: ", s] | |
275 | | Expect s => concat ["Expect: ", s] | |
276 | | Expires s => concat ["Expires: ", s] | |
277 | | Extension {name, value} => concat [name, ": ", value] | |
278 | | From s => concat ["From: ", s] | |
279 | | Host s => concat ["Host: ", s] | |
280 | | IfMatch s => concat ["If-Match: ", s] | |
281 | | LastModified s => concat ["Last-Modified: ", s] | |
282 | | Location s => concat ["Location: ", s] | |
283 | | Pragma s => concat ["Pragma: ", s] | |
284 | | ProxyAuthenticate s => concat ["Proxy-Authenticate: ", s] | |
285 | | ProxyConnection s => concat ["Proxy-Connection: ", s] | |
286 | | Referer s => concat ["Referer: ", s] | |
287 | | RetryAfter s => concat ["Retry-After: ", s] | |
288 | | Server s => concat ["Server: ", s] | |
289 | | SetCookie s => concat ["Set-Cookie: ", s] | |
290 | | Trailer s => concat ["Trailer: ", s] | |
291 | | TransferEncoding s => concat ["Transfer-Encoding: ", s] | |
292 | | Upgrade s => concat ["Upgrade: ", s] | |
293 | | UserAgent s => concat ["User-Agent: ", s] | |
294 | | Vary s => concat ["Vary: ", s] | |
295 | | Via s => concat ["Via: ", s] | |
296 | | WWWAuthenticate s => concat ["WWW-Authenticate: ", s] | |
297 | | Warning s => concat ["Warning: ", s] | |
298 | ||
299 | val layout = Layout.str o toString | |
300 | ||
301 | fun toStrings (hs: t list): string list = | |
302 | List.concatMap (hs, fn h => [toString h, "\r\n"]) | |
303 | ||
304 | val cons: string -> string -> t option = | |
305 | String.memoizeList | |
306 | (fn s => fn s' => SOME (Extension {name = s, value = s'}), | |
307 | [("accept", SOME o Accept), | |
308 | ("accept-charset", SOME o AcceptCharset), | |
309 | ("accept-encoding", SOME o AcceptEncoding), | |
310 | ("accept-language", SOME o AcceptLanguage), | |
311 | ("accept-ranges", SOME o AcceptRanges), | |
312 | ("age", SOME o Age), | |
313 | ("allow", SOME o Allow), | |
314 | ("authorization", | |
315 | let | |
316 | open Regexp | |
317 | val enc = Save.new () | |
318 | val reg = compileNFA (seq [string "Basic ", save (anys, enc)]) | |
319 | in | |
320 | fn s => | |
321 | let | |
322 | in Option.map | |
323 | (Compiled.matchAll (reg, s), fn m => | |
324 | Authorization | |
325 | (Base64.decode (Match.lookupString (m, enc)))) | |
326 | end | |
327 | end), | |
328 | ("cache-control", SOME o CacheControl), | |
329 | ("connection", SOME o Connection), | |
330 | ("content-encoding", SOME o ContentEncoding), | |
331 | ("content-language", SOME o ContentLanguage), | |
332 | ("content-length", | |
333 | fn (s: string) => | |
334 | let open Regexp | |
335 | in if Regexp.Compiled.matchesAll (contentLength (), s) | |
336 | then Option.map (Int.fromString s, ContentLength) | |
337 | else NONE | |
338 | end), | |
339 | ("content-location", SOME o ContentLocation), | |
340 | ("content-md5", SOME o ContentMD5), | |
341 | ("content-range", SOME o ContentRange), | |
342 | ("content-type", SOME o ContentType), | |
343 | ("cookie", SOME o Cookie), | |
344 | ("date", SOME o Date), | |
345 | ("etag", SOME o ETag), | |
346 | ("expect", SOME o Expect), | |
347 | ("expires", SOME o Expires), | |
348 | ("from", SOME o From), | |
349 | ("host", SOME o Host), | |
350 | ("if-match", SOME o IfMatch), | |
351 | ("last-modified", SOME o LastModified), | |
352 | ("location", SOME o Location), | |
353 | ("pragma", SOME o Pragma), | |
354 | ("proxy-authenticate", SOME o ProxyAuthenticate), | |
355 | ("proxy-connection", SOME o ProxyConnection), | |
356 | ("referer", SOME o Referer), | |
357 | ("retry-after", SOME o RetryAfter), | |
358 | ("server", SOME o Server), | |
359 | ("set-cookie", SOME o SetCookie), | |
360 | ("trailer", SOME o Trailer), | |
361 | ("transfer-encoding", SOME o TransferEncoding), | |
362 | ("upgrade", SOME o Upgrade), | |
363 | ("user-agent", SOME o UserAgent), | |
364 | ("vary", SOME o Vary), | |
365 | ("via", SOME o Via), | |
366 | ("www-authenticate", SOME o WWWAuthenticate), | |
367 | ("warning", SOME o Warning)]) | |
368 | ||
369 | fun fromString (s: string): t list Result.t = | |
370 | let | |
371 | val no = Result.No (concat ["invalid header: ", s]) | |
372 | val n = String.size s | |
373 | fun loop (i: int, ac: t list) = | |
374 | if i = n | |
375 | then Result.Yes (rev ac) | |
376 | else let open Regexp | |
377 | in case Compiled.matchLong (messageheader (), s, i) of | |
378 | NONE => no | |
379 | | SOME m => | |
380 | let | |
381 | val {lookup, ...} = Match.stringFuns m | |
382 | val fieldname = String.toLower (lookup fieldname') | |
383 | val fieldvalue = | |
384 | String.dropl (lookup fieldvalue', Char.isSpace) | |
385 | in case cons fieldname fieldvalue of | |
386 | NONE => no | |
387 | | SOME header => | |
388 | loop (i + Match.length m, header :: ac) | |
389 | end | |
390 | end | |
391 | in loop (0, []) | |
392 | end | |
393 | ||
394 | val fromString = | |
395 | Trace.trace ("Http.Header.fromString", | |
396 | String.layout, | |
397 | Result.layout (List.layout layout)) | |
398 | fromString | |
399 | ||
400 | fun input (ins: In.t): t list Result.t = | |
401 | let | |
402 | fun loop (headers: string list): string list = | |
403 | case In.inputLine ins of | |
404 | NONE => headers | |
405 | | SOME l => | |
406 | if l = "\r\n" | |
407 | then headers | |
408 | else loop (l :: headers) | |
409 | in | |
410 | fromString (concat (rev (loop []))) | |
411 | end | |
412 | end | |
413 | ||
414 | structure Request = | |
415 | struct | |
416 | datatype t = T of {method: Method.t, | |
417 | url: RequestUrl.t, | |
418 | version: Version.t, | |
419 | headers: Header.t list} | |
420 | ||
421 | val regexp = Regexp.requestLine | |
422 | ||
423 | fun toString (T {method, url, version, headers}) = | |
424 | concat ([Method.toString method, | |
425 | " ", | |
426 | RequestUrl.toString url, | |
427 | " ", | |
428 | Version.toString version, | |
429 | "\r\n"] | |
430 | @ Header.toStrings headers | |
431 | @ ["\r\n"]) | |
432 | ||
433 | val layout = Layout.str o toString | |
434 | ||
435 | fun output (r, out) = Out.output (out, toString r) | |
436 | ||
437 | fun requestLine (s: string) = | |
438 | let | |
439 | open Regexp | |
440 | in Option.map | |
441 | (Compiled.matchAll (requestLine (), s), fn m => | |
442 | let | |
443 | val {peek, lookup, exists, ...} = Match.stringFuns m | |
444 | val method = Method.fromString (lookup method') | |
445 | open RequestUrl | |
446 | val url = | |
447 | if exists star' | |
448 | then Star | |
449 | else if exists absoluteUrl' | |
450 | then Url (Url.getMatch m) | |
451 | else | |
452 | (case peek authority' of | |
453 | NONE => | |
454 | Path {path = Url.Regexp.getAbsPath m, | |
455 | query = Url.Regexp.peekQuery m} | |
456 | | SOME s => Authority s) | |
457 | val version = Version.extract m | |
458 | in {method = method, | |
459 | url = url, | |
460 | version = version} | |
461 | end) | |
462 | end | |
463 | ||
464 | val requestLine = | |
465 | Trace.trace ("Http.Request.requestLine", | |
466 | String.layout, | |
467 | Option.layout (fn {method, url, version} => | |
468 | Layout.record | |
469 | [("method", Method.layout method), | |
470 | ("url", RequestUrl.layout url), | |
471 | ("version", Version.layout version)])) | |
472 | requestLine | |
473 | ||
474 | val requestIsValid = Option.isSome o requestLine | |
475 | ||
476 | fun input (ins: In.t): t Result.t = | |
477 | case In.inputLine ins of | |
478 | NONE => Result.No "" | |
479 | | SOME l => | |
480 | case requestLine l of | |
481 | NONE => Result.No l | |
482 | | SOME {method, url, version} => | |
483 | Result.map | |
484 | (Header.input ins, fn hs => | |
485 | T {method = method, | |
486 | url = url, | |
487 | version = version, | |
488 | headers = hs}) | |
489 | ||
490 | val input = | |
491 | Trace.trace ("Http.Request.input", In.layout, Result.layout layout) input | |
492 | end | |
493 | ||
494 | structure Rope = | |
495 | struct | |
496 | datatype t = | |
497 | Appends of t list | |
498 | | File of File.t | |
499 | | String of string | |
500 | ||
501 | val appends = Appends | |
502 | val file = File | |
503 | val string = String | |
504 | ||
505 | val empty = String "" | |
506 | ||
507 | fun sizePlus (r: t, ac: int): int = | |
508 | case r of | |
509 | Appends rs => List.fold (rs, ac, sizePlus) | |
510 | | File f => ac + Position.toInt (File.size f) | |
511 | | String s => ac + String.size s | |
512 | ||
513 | fun size (r: t): int = sizePlus (r, 0) | |
514 | ||
515 | fun toStrings (r: t, ac: string list): string list = | |
516 | case r of | |
517 | Appends rs => List.fold (rev rs, ac, toStrings) | |
518 | | File f => File.contents f :: ac | |
519 | | String s => s :: ac | |
520 | ||
521 | fun toString (r: t): string = concat (toStrings (r, [])) | |
522 | ||
523 | fun output (r: t, out: Out.t): unit = | |
524 | let | |
525 | fun loop (r: t): unit = | |
526 | case r of | |
527 | Appends rs => List.foreach (rs, loop) | |
528 | | File f => File.outputContents (f, out) | |
529 | | String s => Out.output (out, s) | |
530 | in | |
531 | loop r | |
532 | end | |
533 | end | |
534 | ||
535 | structure Post = | |
536 | struct | |
537 | structure Encoding = | |
538 | struct | |
539 | datatype t = Url | Multipart | |
540 | end | |
541 | ||
542 | structure Value = | |
543 | struct | |
544 | datatype t = | |
545 | File of File.t | |
546 | | String of string | |
547 | ||
548 | val file = File | |
549 | val string = String | |
550 | ||
551 | fun toString (v: t): string = | |
552 | case v of | |
553 | File f => File.contents f | |
554 | | String s => s | |
555 | ||
556 | fun toRope (v: t): Rope.t = | |
557 | case v of | |
558 | File f => Rope.file f | |
559 | | String s => Rope.string s | |
560 | end | |
561 | ||
562 | datatype t = | |
563 | T of {encoding: Encoding.t, | |
564 | fields: {name: string, | |
565 | value: Value.t} list} | |
566 | ||
567 | fun dquote s = concat ["\"", s, "\""] | |
568 | ||
569 | fun encode (T {encoding, fields}): {contentType: string} * Rope.t = | |
570 | case encoding of | |
571 | Encoding.Url => | |
572 | ({contentType = "application/x-www-form-urlencoded"}, | |
573 | List.fold | |
574 | (rev fields, Rope.empty, fn ({name, value}, r) => | |
575 | let | |
576 | val value = | |
577 | String.translate | |
578 | (Value.toString value, fn c => | |
579 | if Char.isAlphaNum c | |
580 | then Char.toString c | |
581 | else | |
582 | (case c of | |
583 | #" " => "+" | |
584 | | #"\n" => "%0D%0A" | |
585 | | _ => Url.Char.escapeHex c)) | |
586 | in | |
587 | Rope.appends [Rope.string (concat [name, "="]), | |
588 | Rope.string value, | |
589 | Rope.string "&", | |
590 | r] | |
591 | end)) | |
592 | | Encoding.Multipart => | |
593 | let | |
594 | val boundary = | |
595 | String.tabulate | |
596 | (56, fn i => | |
597 | if i < 28 then #"-" else Random.charFrom "0123456789") | |
598 | in | |
599 | ({contentType = concat ["multipart/form-data; boundary=", | |
600 | boundary]}, | |
601 | List.foldr | |
602 | (fields, Rope.string (concat ["--", boundary, "--"]), | |
603 | fn ({name, value}, rope) => | |
604 | let | |
605 | val extra = | |
606 | case value of | |
607 | Value.File f => concat ["; filename=", dquote f] | |
608 | | Value.String _ => "" | |
609 | in | |
610 | Rope.appends | |
611 | [Rope.string | |
612 | (concat | |
613 | ["--", boundary, "\r\n", | |
614 | "Content-Disposition: form-data; name=", dquote name, | |
615 | extra, "\r\n\r\n"]), | |
616 | Value.toRope value, Rope.string "\r\n", rope] | |
617 | end)) | |
618 | end | |
619 | end | |
620 | ||
621 | (* ------------------------------------------------- *) | |
622 | (* fetch *) | |
623 | (* ------------------------------------------------- *) | |
624 | ||
625 | structure Path = Url.Path | |
626 | ||
627 | fun fetch {head: bool, | |
628 | headers: Header.t list, | |
629 | post: Post.t option, | |
630 | proxy: {host: string, port: int} option, | |
631 | url: Url.t}: In.t = | |
632 | let | |
633 | open Url | |
634 | in | |
635 | case url of | |
636 | Url.T {authority = SOME {user, host, port}, | |
637 | fragment, path, query, | |
638 | scheme = SOME Scheme.Http} => | |
639 | let | |
640 | val headers = Header.Host host :: headers | |
641 | val (method, headers, postit) = | |
642 | case post of | |
643 | NONE => | |
644 | (if head then Method.Head else Method.Get, | |
645 | headers, | |
646 | fn _ => ()) | |
647 | | SOME post => | |
648 | let | |
649 | datatype z = datatype Post.Encoding.t | |
650 | val ({contentType}, rope) = Post.encode post | |
651 | val headers = | |
652 | headers | |
653 | @ [Header.ContentType contentType, | |
654 | Header.ContentLength (Rope.size rope)] | |
655 | in | |
656 | (Method.Post, headers, | |
657 | fn out => (Rope.output (rope, out) | |
658 | ; Out.output (out, "\r\n"))) | |
659 | end | |
660 | val (scheme, authority) = | |
661 | if Option.isSome proxy | |
662 | then (SOME Scheme.Http, | |
663 | SOME {user = NONE, | |
664 | host = host, | |
665 | port = port}) | |
666 | else (NONE, NONE) | |
667 | val url = | |
668 | Url.T {scheme = scheme, | |
669 | authority = authority, | |
670 | path = path, | |
671 | query = query, | |
672 | fragment = NONE} | |
673 | val headers = | |
674 | case user of | |
675 | NONE => headers | |
676 | | SOME user => Header.Authorization user :: headers | |
677 | val request = | |
678 | Request.T {method = method, | |
679 | url = RequestUrl.Url url, | |
680 | version = Version.v10, | |
681 | headers = headers} | |
682 | val (ins, out) = | |
683 | Net.connect (case proxy of | |
684 | NONE => {host = host, | |
685 | port = (case port of | |
686 | NONE => 80 | |
687 | | SOME p => p)} | |
688 | | SOME hp => hp) | |
689 | val print = Out.outputc out | |
690 | val () = Request.output (request, out) | |
691 | val () = postit out | |
692 | val () = Out.close out | |
693 | in ins | |
694 | end | |
695 | | _ => Error.bug (concat ["Htt.fetch: ", Url.toString url]) | |
696 | end | |
697 | ||
698 | val fetch = | |
699 | Trace.trace ("Http.fetch", fn {url, ...} => Url.layout url, Layout.ignore) | |
700 | fetch | |
701 | ||
702 | (* ------------------------------------------------- *) | |
703 | (* Status *) | |
704 | (* ------------------------------------------------- *) | |
705 | ||
706 | structure Status = | |
707 | struct | |
708 | datatype t = | |
709 | Accepted | |
710 | | BadGateway | |
711 | | BadRequest | |
712 | | Conflict | |
713 | | Continue | |
714 | | Created | |
715 | | ExpectationFailed | |
716 | | Extension of string | |
717 | | Forbidden | |
718 | | Found | |
719 | | GatewayTimeout | |
720 | | Gone | |
721 | | HTTPVersionNotSupported | |
722 | | InternalServerError | |
723 | | LengthRequired | |
724 | | MethodNotAllowed | |
725 | | MovedPermanently | |
726 | | MultipleChoices | |
727 | | NoContent | |
728 | | NonAuthoritativeInformation | |
729 | | NotAcceptable | |
730 | | NotFound | |
731 | | NotImplemented | |
732 | | NotModified | |
733 | | OK | |
734 | | PartialContent | |
735 | | PaymentRequired | |
736 | | PreconditionFailed | |
737 | | ProxyAuthenticationRequired | |
738 | | RequestEntityTooLarge | |
739 | | RequestTimeout | |
740 | | RequestUriTooLarge | |
741 | | RequestedRangeNotSatisfiable | |
742 | | ResetContent | |
743 | | SeeOther | |
744 | | ServiceUnavailable | |
745 | | SwitchingProtocols | |
746 | | TemporaryRedirect | |
747 | | Unauthorized | |
748 | | UnsupportedMediaType | |
749 | | UseProxy | |
750 | ||
751 | val all = | |
752 | [(Continue, "100", "Continue"), | |
753 | (SwitchingProtocols, "101", "Switching Protocols"), | |
754 | (OK, "200", "OK"), | |
755 | (Created, "201", "Created"), | |
756 | (Accepted, "202", "Accepted"), | |
757 | (NonAuthoritativeInformation, "203", "Non-Authoritative Information"), | |
758 | (NoContent, "204", "No Content"), | |
759 | (ResetContent, "205", "Reset Content"), | |
760 | (PartialContent, "206", "Partial Content"), | |
761 | (MultipleChoices, "300", "Multiple Choices"), | |
762 | (MovedPermanently, "301", "Moved Permanently"), | |
763 | (Found, "302", "Found"), | |
764 | (SeeOther, "303", "See Other"), | |
765 | (NotModified, "304", "Not Modified"), | |
766 | (UseProxy, "305", "Use Proxy"), | |
767 | (TemporaryRedirect, "307", "Temporary Redirect"), | |
768 | (BadRequest, "400", "Bad Request"), | |
769 | (Unauthorized, "401", "Unauthorized"), | |
770 | (PaymentRequired, "402", "Payment Required"), | |
771 | (Forbidden, "403", "Forbidden"), | |
772 | (NotFound, "404", "Not Found"), | |
773 | (MethodNotAllowed, "405", "Method Not Allowed"), | |
774 | (NotAcceptable, "406", "Not Acceptable"), | |
775 | (ProxyAuthenticationRequired, "407", "Proxy Authentication Required"), | |
776 | (RequestTimeout, "408", "Request Time-out"), | |
777 | (Conflict, "409", "Conflict"), | |
778 | (Gone, "410", "Gone"), | |
779 | (LengthRequired, "411", "Length Required"), | |
780 | (PreconditionFailed, "412", "Precondition Failed"), | |
781 | (RequestEntityTooLarge, "413", "Request Entity Too Large"), | |
782 | (RequestUriTooLarge, "414", "Request-URI Too Large"), | |
783 | (UnsupportedMediaType, "415", "Unsupported Media Type"), | |
784 | (RequestedRangeNotSatisfiable, "416", | |
785 | "Requested range not satisfiable"), | |
786 | (ExpectationFailed, "417", "Expectation Failed"), | |
787 | (InternalServerError, "500", "Internal Server Error"), | |
788 | (NotImplemented, "501", "Not Implemented"), | |
789 | (BadGateway, "502", "Bad Gateway"), | |
790 | (ServiceUnavailable, "503", "Service Unavailable"), | |
791 | (GatewayTimeout, "504", "Gateway Time-out"), | |
792 | (HTTPVersionNotSupported, "505", "HTTP Version not supported")] | |
793 | ||
794 | val all = | |
795 | List.revMap (all, fn (status, code, reason) => | |
796 | {status = status, | |
797 | code = code, | |
798 | reason = reason}) | |
799 | ||
800 | fun fromString s = | |
801 | case List.peek (all, fn {code, ...} => s = code) of | |
802 | NONE => Extension s | |
803 | | SOME {status, ...} => status | |
804 | ||
805 | local | |
806 | fun make (ext, sel) (s: t) = | |
807 | case s of | |
808 | Extension c => ext c | |
809 | | _ => sel (valOf (List.peek (all, fn {status, ...} => s = status))) | |
810 | in | |
811 | val code = make (fn c => c, #code) | |
812 | val reason = make (fn _ => "Extension Status Code - No Reason", | |
813 | #reason) | |
814 | end | |
815 | end | |
816 | ||
817 | (* ------------------------------------------------- *) | |
818 | (* Response *) | |
819 | (* ------------------------------------------------- *) | |
820 | ||
821 | structure Response = | |
822 | struct | |
823 | datatype t = T of {version: Version.t, | |
824 | status: Status.t, | |
825 | headers: Header.t list} | |
826 | ||
827 | val regexp = Regexp.responseLine | |
828 | ||
829 | fun toString (T {version, status, headers}) = | |
830 | concat ([Version.toString version, " ", | |
831 | Status.code status, " ", | |
832 | Status.reason status, "\r\n"] | |
833 | @ Header.toStrings headers | |
834 | @ ["\r\n"]) | |
835 | ||
836 | val layout = Layout.str o toString | |
837 | ||
838 | fun output (r, out) = Out.output (out, toString r) | |
839 | ||
840 | fun input (ins: In.t): t Result.t = | |
841 | case In.inputLine ins of | |
842 | NONE => Result.No "" | |
843 | | SOME l => | |
844 | let | |
845 | open Regexp | |
846 | in | |
847 | case Compiled.matchAll (responseLine (), l) of | |
848 | NONE => Result.No l | |
849 | | SOME m => | |
850 | let | |
851 | val {lookup, ...} = Match.stringFuns m | |
852 | val version = Version.extract m | |
853 | val status = Status.fromString (lookup status') | |
854 | in | |
855 | Result.map (Header.input ins, fn hs => | |
856 | T {version = version, | |
857 | status = status, | |
858 | headers = hs}) | |
859 | end | |
860 | end | |
861 | end | |
862 | ||
863 | end |