comparison tango/tango/net/http/HttpClient.d @ 132:1700239cab2e trunk

[svn r136] MAJOR UNSTABLE UPDATE!!! Initial commit after moving to Tango instead of Phobos. Lots of bugfixes... This build is not suitable for most things.
author lindquist
date Fri, 11 Jan 2008 17:57:40 +0100
parents
children
comparison
equal deleted inserted replaced
131:5825d48b27d1 132:1700239cab2e
1 /*******************************************************************************
2
3 copyright: Copyright (c) 2004 Kris Bell. All rights reserved
4
5 license: BSD style: $(LICENSE)
6
7 version: Initial release: April 2004
8 Outback release: December 2006
9
10 author: Kris - original module
11 author: h3r3tic - fixed a number of Post issues and
12 bugs in the 'params' construction
13
14 Redirection handling guided via
15 http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
16
17 *******************************************************************************/
18
19 module tango.net.http.HttpClient;
20
21 private import tango.time.Time;
22
23 private import tango.io.Buffer;
24
25 private import tango.net.Uri,
26 tango.net.SocketConduit,
27 tango.net.InternetAddress;
28
29 private import tango.net.http.HttpConst,
30 tango.net.http.HttpParams,
31 tango.net.http.HttpHeaders,
32 tango.net.http.HttpTriplet,
33 tango.net.http.HttpCookies;
34
35 private import tango.text.stream.LineIterator;
36
37 private import Integer = tango.text.convert.Integer;
38
39
40 /*******************************************************************************
41
42 Supports the basic needs of a client making requests of an HTTP
43 server. The following is an example of how this might be used:
44
45 ---
46 // callback for client reader
47 void sink (char[] content)
48 {
49 Stdout.put (content);
50 }
51
52 // create client for a GET request
53 auto client = new HttpClient (HttpClient.Get, "http://www.yahoo.com");
54
55 // make request
56 client.open ();
57
58 // check return status for validity
59 if (client.isResponseOK)
60 {
61 // extract content length
62 auto length = client.getResponseHeaders.getInt (HttpHeader.ContentLength, uint.max);
63
64 // display all returned headers
65 Stdout.put (client.getResponseHeaders);
66
67 // display remaining content
68 client.read (&sink, length);
69 }
70 else
71 Stderr.put (client.getResponse);
72
73 client.close ();
74 ---
75
76 See modules HttpGet and HttpPost for simple wrappers instead.
77
78 *******************************************************************************/
79
80 class HttpClient
81 {
82 // callback for sending PUT content
83 alias void delegate (IBuffer) Pump;
84
85 // this is struct rather than typedef to avoid compiler bugs
86 private struct RequestMethod
87 {
88 final char[] name;
89 }
90
91 // class members; there's a surprising amount of stuff here!
92 private Uri uri;
93 private IBuffer tmp,
94 input,
95 output;
96 private SocketConduit socket;
97 private RequestMethod method;
98 private InternetAddress address;
99 private HttpParams paramsOut;
100 private HttpHeadersView headersIn;
101 private HttpHeaders headersOut;
102 private HttpCookies cookiesOut;
103 private ResponseLine responseLine;
104
105 // default to three second timeout on read operations ...
106 private TimeSpan timeout = TimeSpan.seconds(3);
107
108 // should we perform internal redirection?
109 private bool doRedirect = true;
110
111 // use HTTP v1.0?
112 private static const char[] DefaultHttpVersion = "HTTP/1.0";
113
114 // standard set of request methods ...
115 static const RequestMethod Get = {"GET"},
116 Put = {"PUT"},
117 Head = {"HEAD"},
118 Post = {"POST"},
119 Trace = {"TRACE"},
120 Delete = {"DELETE"},
121 Options = {"OPTIONS"},
122 Connect = {"CONNECT"};
123
124 /***********************************************************************
125
126 Create a client for the given URL. The argument should be
127 fully qualified with an "http:" or "https:" scheme, or an
128 explicit port should be provided.
129
130 ***********************************************************************/
131
132 this (RequestMethod method, char[] url)
133 {
134 this (method, new Uri(url));
135 }
136
137 /***********************************************************************
138
139 Create a client with the provided Uri instance. The Uri should
140 be fully qualified with an "http:" or "https:" scheme, or an
141 explicit port should be provided.
142
143 ***********************************************************************/
144
145 this (RequestMethod method, Uri uri)
146 {
147 this.uri = uri;
148 this.method = method;
149
150 responseLine = new ResponseLine;
151 headersIn = new HttpHeadersView;
152
153 tmp = new Buffer (1024 * 4);
154 paramsOut = new HttpParams (new Buffer (1024 * 4));
155 headersOut = new HttpHeaders (new Buffer (1024 * 4));
156 cookiesOut = new HttpCookies (headersOut);
157
158 // decode the host name (may take a second or two)
159 auto host = uri.getHost ();
160 if (host)
161 address = new InternetAddress (host, uri.getValidPort);
162 else
163 responseLine.error ("invalid url provided to HttpClient ctor");
164 }
165
166 /***********************************************************************
167
168 Get the current input headers, as returned by the host request.
169
170 ***********************************************************************/
171
172 HttpHeadersView getResponseHeaders()
173 {
174 return headersIn;
175 }
176
177 /***********************************************************************
178
179 Gain access to the request headers. Use this to add whatever
180 headers are required for a request.
181
182 ***********************************************************************/
183
184 HttpHeaders getRequestHeaders()
185 {
186 return headersOut;
187 }
188
189 /***********************************************************************
190
191 Gain access to the request parameters. Use this to add x=y
192 style parameters to the request. These will be appended to
193 the request assuming the original Uri does not contain any
194 of its own.
195
196 ***********************************************************************/
197
198 HttpParams getRequestParams()
199 {
200 return paramsOut;
201 }
202
203 /***********************************************************************
204
205 Return the Uri associated with this client
206
207 ***********************************************************************/
208
209 UriView getUri()
210 {
211 return uri;
212 }
213
214 /***********************************************************************
215
216 Return the response-line for the latest request. This takes
217 the form of "version status reason" as defined in the HTTP
218 RFC.
219
220 ***********************************************************************/
221
222 ResponseLine getResponse()
223 {
224 return responseLine;
225 }
226
227 /***********************************************************************
228
229 Return the HTTP status code set by the remote server
230
231 ***********************************************************************/
232
233 int getStatus()
234 {
235 return responseLine.getStatus;
236 }
237
238 /***********************************************************************
239
240 Return whether the response was OK or not
241
242 ***********************************************************************/
243
244 bool isResponseOK()
245 {
246 return getStatus is HttpResponseCode.OK;
247 }
248
249 /***********************************************************************
250
251 Add a cookie to the outgoing headers
252
253 ***********************************************************************/
254
255 void addCookie (Cookie cookie)
256 {
257 cookiesOut.add (cookie);
258 }
259
260 /***********************************************************************
261
262 Close all resources used by a request. You must invoke this
263 between successive open() calls.
264
265 ***********************************************************************/
266
267 void close ()
268 {
269 if (socket)
270 {
271 socket.shutdown;
272 socket.detach;
273 socket = null;
274 }
275 }
276
277 /***********************************************************************
278
279 Reset the client such that it is ready for a new request.
280
281 ***********************************************************************/
282
283 void reset ()
284 {
285 headersIn.reset;
286 headersOut.reset;
287 paramsOut.reset;
288 }
289
290 /***********************************************************************
291
292 enable/disable the internal redirection suppport
293
294 ***********************************************************************/
295
296 void enableRedirect (bool yes)
297 {
298 doRedirect = yes;
299 }
300
301 /***********************************************************************
302
303 set timeout period for read operation
304
305 ***********************************************************************/
306
307 void setTimeout (TimeSpan interval)
308 {
309 timeout = interval;
310 }
311
312 /**
313 * Deprecated: use setTimeout(TimeSpan) instead
314 */
315 deprecated void setTimeout(double interval)
316 {
317 setTimeout(TimeSpan.interval(interval));
318 }
319
320 /***********************************************************************
321
322 Overridable method to create a Socket. You may find a need
323 to override this for some purpose; perhaps to add input or
324 output filters.
325
326 ***********************************************************************/
327
328 protected SocketConduit createSocket ()
329 {
330 return new SocketConduit;
331 }
332
333 /***********************************************************************
334
335 Make a request for the resource specified via the constructor,
336 using the specified timeout period (in milli-seconds).The
337 return value represents the input buffer, from which all
338 returned headers and content may be accessed.
339
340 ***********************************************************************/
341
342 IBuffer open (IBuffer buffer = null)
343 {
344 return open (method, null, buffer);
345 }
346
347 /***********************************************************************
348
349 Make a request for the resource specified via the constructor,
350 using a callback for pumping additional data to the host. This
351 defaults to a three-second timeout period. The return value
352 represents the input buffer, from which all returned headers
353 and content may be accessed.
354
355 ***********************************************************************/
356
357 IBuffer open (Pump pump, IBuffer buffer = null)
358 {
359 return open (method, pump, buffer);
360 }
361
362 /***********************************************************************
363
364 Make a request for the resource specified via the constructor
365 using the specified timeout period (in micro-seconds), and a
366 user-defined callback for pumping additional data to the host.
367 The callback would be used when uploading data during a 'put'
368 operation (or equivalent). The return value represents the
369 input buffer, from which all returned headers and content may
370 be accessed.
371
372 Note that certain request-headers may generated automatically
373 if they are not present. These include a Host header and, in
374 the case of Post, both ContentType & ContentLength for a query
375 type of request. The latter two are *not* produced for Post
376 requests with 'pump' specified ~ when using 'pump' to output
377 additional content, you must explicitly set your own headers.
378
379 Note also that IOException instances may be thrown. These
380 should be caught by the client to ensure a close() operation
381 is always performed
382
383 ***********************************************************************/
384
385 private IBuffer open (RequestMethod method, Pump pump, IBuffer input)
386 {
387 // create socket, and connect it
388 socket = createSocket;
389 socket.setTimeout (timeout);
390 socket.connect (address);
391
392 // create buffers for input and output
393 if (input)
394 input.clear, input.setConduit (socket);
395 else
396 input = new Buffer (socket);
397 output = new Buffer (socket);
398
399 // save for read() method
400 this.input = input;
401
402 // setup a Host header
403 if (headersOut.get (HttpHeader.Host, null) is null)
404 headersOut.add (HttpHeader.Host, uri.getHost);
405
406 // http/1.0 needs connection:close
407 headersOut.add (HttpHeader.Connection, "close");
408
409 // attach/extend query parameters if user has added some
410 tmp.clear;
411 auto query = uri.extendQuery (paramsOut.formatTokens(tmp, "&"));
412
413 // patch request path?
414 auto path = uri.getPath;
415 if (path.length is 0)
416 path = "/";
417
418 // format encoded request
419 output (method.name) (" "), uri.encode (&output.consume, path, uri.IncPath);
420
421 // should we emit query as part of request line?
422 if (query.length)
423 if (method is Get)
424 output ("?"), uri.encode (&output.consume, query, uri.IncQueryAll);
425 else
426 if (method is Post && pump.funcptr is null)
427 {
428 // we're POSTing query text - add default info
429 if (headersOut.get (HttpHeader.ContentType, null) is null)
430 headersOut.add (HttpHeader.ContentType, "application/x-www-form-urlencoded");
431
432 if (headersOut.get (HttpHeader.ContentLength, null) is null)
433 headersOut.addInt (HttpHeader.ContentLength, query.length);
434 }
435
436 // complete the request line, and emit headers too
437 output (" ") (DefaultHttpVersion) (HttpConst.Eol);
438
439 headersOut.produce (&output.consume, HttpConst.Eol);
440 output (HttpConst.Eol);
441
442 // user has additional data to send?
443 if (pump.funcptr)
444 pump (output);
445 else
446 // send encoded POST query instead?
447 if (method is Post && query.length)
448 uri.encode (&output.consume, query, uri.IncQueryAll);
449
450 // send entire request
451 output.flush;
452
453 // Token for initial parsing of input header lines
454 auto line = new LineIterator!(char) (input);
455
456 // skip any blank lines
457 while (line.next && line.get.length is 0)
458 {}
459
460 // throw if we experienced a timeout
461 if (socket.hadTimeout)
462 responseLine.error ("response timeout");
463
464 // is this a bogus request?
465 if (line.get.length is 0)
466 responseLine.error ("truncated response");
467
468 // read response line
469 responseLine.parse (line.get);
470
471 // parse incoming headers
472 headersIn.reset.parse (input);
473
474 // check for redirection
475 if (doRedirect)
476 switch (responseLine.getStatus)
477 {
478 case HttpResponseCode.SeeOther:
479 case HttpResponseCode.MovedPermanently:
480 case HttpResponseCode.MovedTemporarily:
481 case HttpResponseCode.TemporaryRedirect:
482 // drop this connection
483 close;
484
485 // remove any existing Host header
486 headersOut.remove (HttpHeader.Host);
487
488 // parse redirected uri
489 auto redirect = headersIn.get (HttpHeader.Location, "[missing url]");
490 uri.relParse (redirect);
491
492 // decode the host name (may take a second or two)
493 auto host = uri.getHost();
494 if (host)
495 address = new InternetAddress (uri.getHost, uri.getValidPort);
496 else
497 responseLine.error ("redirect has invalid url: "~redirect);
498
499 // figure out what to do
500 if (method is Get || method is Head)
501 return open (method, pump, input);
502 else
503 if (method is Post)
504 return redirectPost (pump, input, responseLine.getStatus);
505 else
506 responseLine.error ("unexpected redirect for method "~method.name);
507 default:
508 break;
509 }
510
511 // return the input buffer
512 return input;
513 }
514
515 /***********************************************************************
516
517 Read the content from the returning input stream, up to a
518 maximum length, and pass content to the given sink delegate
519 as it arrives.
520
521 Exits when length bytes have been processed, or an Eof is
522 seen on the stream.
523
524 ***********************************************************************/
525
526 void read (void delegate (void[]) sink, long len = long.max)
527 {
528 while (len > 0)
529 {
530 auto content = input.slice;
531 if ((len -= content.length) < 0)
532 {
533 content = content [0 .. $ + cast(size_t) len];
534 sink (content);
535 input.skip (content.length);
536 }
537 else
538 {
539 sink (content);
540 input.clear;
541 if (input.fill(input.input) is socket.Eof)
542 break;
543 }
544 }
545 }
546
547 /***********************************************************************
548
549 Handle redirection of Post
550
551 Guidance for the default behaviour came from this page:
552 http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
553
554 ***********************************************************************/
555
556 IBuffer redirectPost (Pump pump, IBuffer input, int status)
557 {
558 switch (status)
559 {
560 // use Get method to complete the Post
561 case HttpResponseCode.SeeOther:
562 case HttpResponseCode.MovedTemporarily:
563
564 // remove POST headers first!
565 headersOut.remove (HttpHeader.ContentLength);
566 headersOut.remove (HttpHeader.ContentType);
567 paramsOut.reset;
568 return open (Get, null, input);
569
570 // try entire Post again, if user say OK
571 case HttpResponseCode.MovedPermanently:
572 case HttpResponseCode.TemporaryRedirect:
573 if (canRepost (status))
574 return open (this.method, pump, input);
575 // fall through!
576
577 default:
578 responseLine.error ("Illegal redirection of Post");
579 }
580 return null;
581 }
582
583 /***********************************************************************
584
585 Handle user-notification of Post redirection. This should
586 be overridden appropriately.
587
588 Guidance for the default behaviour came from this page:
589 http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
590
591 ***********************************************************************/
592
593 bool canRepost (uint status)
594 {
595 return false;
596 }
597 }
598
599
600 /******************************************************************************
601
602 Class to represent an HTTP response-line
603
604 ******************************************************************************/
605
606 private class ResponseLine : HttpTriplet
607 {
608 private char[] vers,
609 reason;
610 private int status;
611
612 /**********************************************************************
613
614 test the validity of these tokens
615
616 **********************************************************************/
617
618 void test ()
619 {
620 vers = tokens[0];
621 reason = tokens[2];
622 status = cast(int) Integer.convert (tokens[1]);
623 if (status is 0)
624 {
625 status = cast(int) Integer.convert (tokens[2]);
626 if (status is 0)
627 error ("Invalid HTTP response: '"~tokens[0]~"' '"~tokens[1]~"' '" ~tokens[2] ~"'");
628 }
629 }
630
631 /**********************************************************************
632
633 Return HTTP version
634
635 **********************************************************************/
636
637 char[] getVersion ()
638 {
639 return vers;
640 }
641
642 /**********************************************************************
643
644 Return reason text
645
646 **********************************************************************/
647
648 char[] getReason ()
649 {
650 return reason;
651 }
652
653 /**********************************************************************
654
655 Return status integer
656
657 **********************************************************************/
658
659 int getStatus ()
660 {
661 return status;
662 }
663 }