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