00001 /******************************************************************************* 00002 00003 @file HttpClient.d 00004 00005 Copyright (C) 2004 Kris Bell 00006 00007 This software is provided 'as-is', without any express or implied 00008 warranty. In no event will the authors be held liable for damages 00009 of any kind arising from the use of this software. 00010 00011 Permission is hereby granted to anyone to use this software for any 00012 purpose, including commercial applications, and to alter it and/or 00013 redistribute it freely, subject to the following restrictions: 00014 00015 1. The origin of this software must not be misrepresented; you must 00016 not claim that you wrote the original software. If you use this 00017 software in a product, an acknowledgment within documentation of 00018 said product would be appreciated but is not required. 00019 00020 2. Altered source versions must be plainly marked as such, and must 00021 not be misrepresented as being the original software. 00022 00023 3. This notice may not be removed or altered from any distribution 00024 of the source. 00025 00026 00027 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 00028 00029 00030 @version Initial version, April 2004 00031 @author Kris 00032 00033 00034 *******************************************************************************/ 00035 00036 module mango.http.client.HttpClient; 00037 00038 private import mango.base.System; 00039 00040 private import mango.utils.Text; 00041 00042 private import mango.io.Uri, 00043 mango.io.Token, 00044 mango.io.Buffer, 00045 mango.io.SocketConduit, 00046 mango.io.Exception, 00047 mango.io.Tokenizer, 00048 mango.io.DisplayWriter; 00049 00050 private import mango.http.server.HttpParams, 00051 mango.http.server.HttpHeaders, 00052 mango.http.server.HttpCookies, 00053 mango.http.server.HttpResponse; 00054 00055 /******************************************************************************* 00056 00057 Supports the basic needs of a client making requests of an HTTP 00058 server. The following is an example of how this might be used: 00059 00060 @code 00061 // callback for client reader 00062 void sink (char[] content) 00063 { 00064 Stdout.put (content); 00065 } 00066 00067 // create client for a GET request 00068 auto HttpClient client = new HttpClient (HttpClient.Get, "http://www.digitalmars.com/d/intro.html"); 00069 00070 // setup a Host header 00071 client.getRequestHeaders.add (HttpHeader.Host, client.getUri.getHost()); 00072 00073 // make request 00074 client.open (); 00075 00076 // check return status for validity 00077 if (client.isResponseOK) 00078 { 00079 // extract content length (be aware of -1 return, for no header) 00080 int length = client.getResponseHeaders.getInt (HttpHeader.ContentLength); 00081 if (length < 0) 00082 length = int.max; 00083 00084 // display all returned headers 00085 Stdout.put (client.getResponseHeaders); 00086 00087 // display remaining content 00088 client.read (&sink, length); 00089 } 00090 else 00091 Stderr.put (client.getResponse); 00092 @endcode 00093 00094 *******************************************************************************/ 00095 00096 class HttpClient 00097 { 00098 // this is struct rather than typedef to avoid compiler bugs 00099 private struct RequestMethod 00100 { 00101 final char[] name; 00102 } 00103 00104 // class members; there's a surprising amount of stuff here! 00105 private MutableUri uri; 00106 private IBuffer input, 00107 output; 00108 private SocketConduit socket; 00109 private RequestMethod method; 00110 private InternetAddress address; 00111 private HttpMutableParams paramsOut; 00112 private HttpHeaders headersIn; 00113 private HttpMutableHeaders headersOut; 00114 private HttpMutableCookies cookiesOut; 00115 private ResponseLine responseLine; 00116 00117 // default to three second timeout on read operations ... 00118 private static final uint DefaultReadTimeout = System.Interval.Second * 3; 00119 00120 // use HTTP v1.0 ? 00121 private static const char[] DefaultHttpVersion = "HTTP/1.1"; 00122 00123 // standard set of request methods ... 00124 static const RequestMethod Get = {"GET"}, 00125 Put = {"PUT"}, 00126 Head = {"HEAD"}, 00127 Post = {"POST"}, 00128 Trace = {"TRACE"}, 00129 Delete = {"DELETE"}, 00130 Options = {"OPTIONS"}, 00131 Connect = {"CONNECT"}; 00132 00133 /*********************************************************************** 00134 00135 Create a client for the given URL. The argument should be 00136 fully qualified with an "http:" or "https:" scheme, or an 00137 explicit port should be provided. 00138 00139 ***********************************************************************/ 00140 00141 this (RequestMethod method, char[] url) 00142 { 00143 this (method, new MutableUri(url)); 00144 } 00145 00146 /*********************************************************************** 00147 00148 Create a client with the provided Uri instance. The Uri should 00149 be fully qualified with an "http:" or "https:" scheme, or an 00150 explicit port should be provided. 00151 00152 ***********************************************************************/ 00153 00154 this (RequestMethod method, MutableUri uri) 00155 { 00156 this.uri = uri; 00157 this.method = method; 00158 00159 responseLine = new ResponseLine (); 00160 headersIn = new HttpHeaders (); 00161 00162 paramsOut = new HttpMutableParams (new Buffer (1024 * 1)); 00163 headersOut = new HttpMutableHeaders (new Buffer (1024 * 4)); 00164 cookiesOut = new HttpMutableCookies (headersOut); 00165 00166 // decode the host name (may take a second or two) 00167 address = new InternetAddress (uri.getHost(), uri.getValidPort()); 00168 } 00169 00170 /*********************************************************************** 00171 00172 Attempt to clean up when garbage collected 00173 00174 ***********************************************************************/ 00175 00176 ~this () 00177 { 00178 close (); 00179 } 00180 00181 /*********************************************************************** 00182 00183 Get the current input headers, as returned by the host request. 00184 00185 ***********************************************************************/ 00186 00187 HttpHeaders getResponseHeaders() 00188 { 00189 return headersIn; 00190 } 00191 00192 /*********************************************************************** 00193 00194 Gain access to the request headers. Use this to add whatever 00195 headers are required for a request. 00196 00197 ***********************************************************************/ 00198 00199 HttpMutableHeaders getRequestHeaders() 00200 { 00201 return headersOut; 00202 } 00203 00204 /*********************************************************************** 00205 00206 Gain access to the request parameters. Use this to add x=y 00207 style parameters to the request. These will be appended to 00208 the request assuming the original Uri does not contain any 00209 of its own. 00210 00211 ***********************************************************************/ 00212 00213 HttpMutableParams getRequestParams() 00214 { 00215 return paramsOut; 00216 } 00217 00218 /*********************************************************************** 00219 00220 Return the Uri associated with this client 00221 00222 ***********************************************************************/ 00223 00224 Uri getUri() 00225 { 00226 return uri; 00227 } 00228 00229 /*********************************************************************** 00230 00231 Return the response-line for the latest request. This takes 00232 the form of "version status reason" as defined in the HTTP 00233 RFC. 00234 00235 ***********************************************************************/ 00236 00237 ResponseLine getResponse() 00238 { 00239 return responseLine; 00240 } 00241 00242 /*********************************************************************** 00243 00244 Return the HTTP status code set by the remote server 00245 00246 ***********************************************************************/ 00247 00248 int getStatus() 00249 { 00250 return responseLine.getStatus(); 00251 } 00252 00253 /*********************************************************************** 00254 00255 Return whether the response was OK or not 00256 00257 ***********************************************************************/ 00258 00259 bool isResponseOK() 00260 { 00261 return getStatus() == HttpResponseCode.OK; 00262 } 00263 00264 /*********************************************************************** 00265 00266 Add a cookie to the outgoing headers 00267 00268 ***********************************************************************/ 00269 00270 void addCookie (Cookie cookie) 00271 { 00272 cookiesOut.add (cookie); 00273 } 00274 00275 /*********************************************************************** 00276 00277 Close all resources used by a request. You must invoke this 00278 between successive open() calls. 00279 00280 ***********************************************************************/ 00281 00282 void close () 00283 { 00284 if (socket) 00285 { 00286 socket.shutdown (); 00287 socket.close (); 00288 socket = null; 00289 } 00290 } 00291 00292 /*********************************************************************** 00293 00294 Reset the client such that it is ready for a new request. 00295 00296 ***********************************************************************/ 00297 00298 void reset () 00299 { 00300 headersIn.reset(); 00301 headersOut.reset(); 00302 paramsOut.reset(); 00303 } 00304 00305 /*********************************************************************** 00306 00307 Overridable method to create a Socket. You may find a need 00308 to override this for some purpose; perhaps to add input or 00309 output filters. 00310 00311 ***********************************************************************/ 00312 00313 protected SocketConduit createSocket () 00314 { 00315 return new SocketConduit; 00316 } 00317 00318 /*********************************************************************** 00319 00320 Make a request for the resource specified via the constructor, 00321 using a callback for pumping additional data to the host. This 00322 defaults to a three-second timeout period. The return value 00323 represents the input buffer, from which all returned headers 00324 and content may be accessed. 00325 00326 ***********************************************************************/ 00327 00328 IBuffer open (IWritable pump) 00329 { 00330 return open (DefaultReadTimeout, pump); 00331 } 00332 00333 /*********************************************************************** 00334 00335 Make a request for the resource specified via the constructor, 00336 using the specified timeout period (in milli-seconds).The 00337 return value represents the input buffer, from which all 00338 returned headers and content may be accessed. 00339 00340 ***********************************************************************/ 00341 00342 IBuffer open (uint timeout = DefaultReadTimeout) 00343 { 00344 return open (timeout, null); 00345 } 00346 00347 /*********************************************************************** 00348 00349 Make a request for the resource specified via the constructor 00350 using the specified timeout period (in micro-seconds), and a 00351 user-defined callback for pumping additional data to the host. 00352 The callback would be used when uploading data during a 'put' 00353 operation (or equivalent). The return value represents the 00354 input buffer, from which all returned headers and content may 00355 be accessed. 00356 00357 ***********************************************************************/ 00358 00359 IBuffer open (uint timeout, IWritable pump) 00360 { 00361 IWriter writer; 00362 00363 // create socket, and connect it 00364 socket = createSocket; 00365 socket.setTimeout (timeout); 00366 socket.connect (address); 00367 00368 // create buffers for input and output 00369 input = socket.createBuffer; 00370 output = socket.createBuffer; 00371 00372 // bind a writer to the socket 00373 writer = new DisplayWriter (output); 00374 00375 // attach query parameters if user has added some 00376 char[] query = paramsOut.toOutputString; 00377 if (query.length) 00378 uri.setQuery (query); 00379 00380 // format request 00381 writer.put (method.name) 00382 .put (' ') 00383 .put (uri.getPath) 00384 .put (' ') 00385 .put (DefaultHttpVersion) 00386 .cr () 00387 .put (headersOut) 00388 .cr (); 00389 00390 // user has more data to send? 00391 if (pump) 00392 pump.write (writer); 00393 00394 // send entire request 00395 writer.flush (); 00396 00397 // Token for initial parsing of input header lines 00398 CompositeToken line = new CompositeToken (Tokenizers.line, input); 00399 00400 // skip any blank lines 00401 while (line.next() && line.getLength() == 0) 00402 {} 00403 00404 // is this a bogus request? 00405 if (input.readable == 0) 00406 throw new IOException ("truncated response"); 00407 00408 // read response line 00409 responseLine.parse (line.toString); 00410 00411 // parse headers and go home 00412 headersIn.parse (input); 00413 return input; 00414 } 00415 00416 /*********************************************************************** 00417 00418 ***********************************************************************/ 00419 00420 void read (void delegate (char[]) sink, long length = long.max) 00421 { 00422 // display remaining content 00423 do { 00424 length -= input.readable; 00425 sink (input.toString); 00426 input.clear (); 00427 } while (length > 0 && socket.read(input) != socket.Eof); 00428 } 00429 } 00430 00431 00432 /****************************************************************************** 00433 00434 Class to represent an HTTP response-line 00435 00436 ******************************************************************************/ 00437 00438 private class ResponseLine : IWritable 00439 { 00440 private Buffer buf; 00441 private BoundToken token; 00442 private char[] reason, 00443 status, 00444 vershion; 00445 00446 /********************************************************************** 00447 00448 Construct response-line with pre-allocated buffer & token 00449 00450 **********************************************************************/ 00451 00452 this () 00453 { 00454 buf = new Buffer; 00455 00456 // bind tokens to a space-tokenizer 00457 token = new BoundToken (new SpaceTokenizer()); 00458 } 00459 00460 /********************************************************************** 00461 00462 Parse the the given response-line into its constituent 00463 components. 00464 00465 **********************************************************************/ 00466 00467 void parse (char[] line) 00468 { 00469 // setup our buffer with the request 00470 buf.setValidContent (line); 00471 00472 if (token.next (buf)) 00473 { 00474 vershion = token.toString(); 00475 if (token.next (buf)) 00476 { 00477 status = token.toString(); 00478 reason = line[buf.getPosition..line.length]; 00479 return; 00480 } 00481 } 00482 throw new IOException ("Invalid HTTP response-line: '"~line~"'"); 00483 } 00484 00485 /********************************************************************** 00486 00487 Return HTTP version 00488 00489 **********************************************************************/ 00490 00491 char[] getVersion () 00492 { 00493 return vershion; 00494 } 00495 00496 /********************************************************************** 00497 00498 Return reason text 00499 00500 **********************************************************************/ 00501 00502 char[] getReason () 00503 { 00504 return reason; 00505 } 00506 00507 /********************************************************************** 00508 00509 Return status integer 00510 00511 **********************************************************************/ 00512 00513 int getStatus () 00514 { 00515 return Text.atoi(status); 00516 } 00517 00518 /********************************************************************** 00519 00520 convert back to original string 00521 00522 **********************************************************************/ 00523 00524 override char[] toString () 00525 { 00526 return vershion~" "~status~" "~reason; 00527 } 00528 00529 /********************************************************************** 00530 00531 Output the string via the given writer 00532 00533 **********************************************************************/ 00534 00535 void write (IWriter writer) 00536 { 00537 writer.put(toString).cr(); 00538 } 00539 } 00540 00541