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