Import Upstream version 20180207
[hcoop/debian/mlton.git] / lib / mlton / basic / http.sml
CommitLineData
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
9structure Http: HTTP =
10struct
11
12structure 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
118structure 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
154structure 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
180structure 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
205exception ParseError
206
207structure 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
414structure 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
494structure 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
535structure 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
625structure Path = Url.Path
626
627fun 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
698val fetch =
699 Trace.trace ("Http.fetch", fn {url, ...} => Url.layout url, Layout.ignore)
700 fetch
701
702(* ------------------------------------------------- *)
703(* Status *)
704(* ------------------------------------------------- *)
705
706structure 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
821structure 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
863end