diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tango/tango/net/http/HttpClient.d	Fri Jan 11 17:57:40 2008 +0100
@@ -0,0 +1,663 @@
+/*******************************************************************************
+
+        copyright:      Copyright (c) 2004 Kris Bell. All rights reserved
+
+        license:        BSD style: $(LICENSE)
+
+        version:        Initial release: April 2004      
+                        Outback release: December 2006
+        
+        author:         Kris    - original module
+        author:         h3r3tic - fixed a number of Post issues and
+                                  bugs in the 'params' construction
+
+        Redirection handling guided via 
+                    http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
+
+*******************************************************************************/
+
+module tango.net.http.HttpClient;
+
+private import  tango.time.Time;
+                
+private import  tango.io.Buffer;
+
+private import  tango.net.Uri,
+                tango.net.SocketConduit,
+                tango.net.InternetAddress;
+
+private import  tango.net.http.HttpConst,
+                tango.net.http.HttpParams,  
+                tango.net.http.HttpHeaders,
+                tango.net.http.HttpTriplet,
+                tango.net.http.HttpCookies;
+
+private import  tango.text.stream.LineIterator;
+
+private import  Integer = tango.text.convert.Integer;
+
+
+/*******************************************************************************
+
+        Supports the basic needs of a client making requests of an HTTP
+        server. The following is an example of how this might be used:
+
+        ---
+        // callback for client reader
+        void sink (char[] content)
+        {
+                Stdout.put (content);
+        }
+
+        // create client for a GET request
+        auto client = new HttpClient (HttpClient.Get, "http://www.yahoo.com");
+
+        // make request
+        client.open ();
+
+        // check return status for validity
+        if (client.isResponseOK)
+           {
+           // extract content length
+           auto length = client.getResponseHeaders.getInt (HttpHeader.ContentLength, uint.max);
+        
+           // display all returned headers
+           Stdout.put (client.getResponseHeaders);
+        
+           // display remaining content
+           client.read (&sink, length);
+           }
+        else
+           Stderr.put (client.getResponse);
+
+        client.close ();
+        ---
+
+        See modules HttpGet and HttpPost for simple wrappers instead.
+
+*******************************************************************************/
+
+class HttpClient
+{       
+        // callback for sending PUT content
+        alias void delegate (IBuffer)   Pump;
+
+        // this is struct rather than typedef to avoid compiler bugs
+        private struct RequestMethod
+        {
+                final char[]            name;
+        }    
+                        
+        // class members; there's a surprising amount of stuff here!
+        private Uri                     uri;
+        private IBuffer                 tmp,
+                                        input,
+                                        output;
+        private SocketConduit           socket;
+        private RequestMethod           method;
+        private InternetAddress         address;
+        private HttpParams              paramsOut;
+        private HttpHeadersView         headersIn;
+        private HttpHeaders             headersOut;
+        private HttpCookies             cookiesOut;
+        private ResponseLine            responseLine;
+
+        // default to three second timeout on read operations ...
+        private TimeSpan                timeout = TimeSpan.seconds(3);
+
+        // should we perform internal redirection?
+        private bool                    doRedirect = true;
+
+        // use HTTP v1.0?
+        private static const char[] DefaultHttpVersion = "HTTP/1.0";
+
+        // standard set of request methods ...
+        static const RequestMethod      Get = {"GET"},
+                                        Put = {"PUT"},
+                                        Head = {"HEAD"},
+                                        Post = {"POST"},
+                                        Trace = {"TRACE"},
+                                        Delete = {"DELETE"},
+                                        Options = {"OPTIONS"},
+                                        Connect = {"CONNECT"};
+
+        /***********************************************************************
+        
+                Create a client for the given URL. The argument should be
+                fully qualified with an "http:" or "https:" scheme, or an
+                explicit port should be provided.
+
+        ***********************************************************************/
+
+        this (RequestMethod method, char[] url)
+        {
+                this (method, new Uri(url));
+        }
+
+        /***********************************************************************
+        
+                Create a client with the provided Uri instance. The Uri should 
+                be fully qualified with an "http:" or "https:" scheme, or an
+                explicit port should be provided. 
+
+        ***********************************************************************/
+
+        this (RequestMethod method, Uri uri)
+        {
+                this.uri = uri;
+                this.method = method;
+
+                responseLine = new ResponseLine;
+                headersIn    = new HttpHeadersView;
+
+                tmp          = new Buffer (1024 * 4);
+                paramsOut    = new HttpParams  (new Buffer (1024 * 4));
+                headersOut   = new HttpHeaders (new Buffer (1024 * 4));
+                cookiesOut   = new HttpCookies (headersOut);
+
+                // decode the host name (may take a second or two)
+                auto host = uri.getHost ();
+                if (host)
+                    address = new InternetAddress (host, uri.getValidPort);
+                else
+                   responseLine.error ("invalid url provided to HttpClient ctor");
+        }
+
+        /***********************************************************************
+        
+                Get the current input headers, as returned by the host request.
+
+        ***********************************************************************/
+
+        HttpHeadersView getResponseHeaders()
+        {
+                return headersIn;
+        }
+
+        /***********************************************************************
+        
+                Gain access to the request headers. Use this to add whatever
+                headers are required for a request. 
+
+        ***********************************************************************/
+
+        HttpHeaders getRequestHeaders()
+        {
+                return headersOut;
+        }
+
+        /***********************************************************************
+        
+                Gain access to the request parameters. Use this to add x=y
+                style parameters to the request. These will be appended to
+                the request assuming the original Uri does not contain any
+                of its own.
+
+        ***********************************************************************/
+
+        HttpParams getRequestParams()
+        {
+                return paramsOut;
+        }
+
+        /***********************************************************************
+        
+                Return the Uri associated with this client
+
+        ***********************************************************************/
+
+        UriView getUri()
+        {
+                return uri;
+        }
+
+        /***********************************************************************
+        
+                Return the response-line for the latest request. This takes 
+                the form of "version status reason" as defined in the HTTP
+                RFC.
+
+        ***********************************************************************/
+
+        ResponseLine getResponse()
+        {
+                return responseLine;
+        }
+
+        /***********************************************************************
+        
+                Return the HTTP status code set by the remote server
+
+        ***********************************************************************/
+
+        int getStatus()
+        {
+                return responseLine.getStatus;
+        }
+
+        /***********************************************************************
+        
+                Return whether the response was OK or not
+
+        ***********************************************************************/
+
+        bool isResponseOK()
+        {
+                return getStatus is HttpResponseCode.OK;
+        }
+
+        /***********************************************************************
+        
+                Add a cookie to the outgoing headers
+
+        ***********************************************************************/
+
+        void addCookie (Cookie cookie)
+        {
+                cookiesOut.add (cookie);
+        }
+
+        /***********************************************************************
+        
+                Close all resources used by a request. You must invoke this 
+                between successive open() calls.
+
+        ***********************************************************************/
+
+        void close ()
+        {
+                if (socket)
+                   {
+                   socket.shutdown;
+                   socket.detach;
+                   socket = null;
+                   }
+        }
+
+        /***********************************************************************
+
+                Reset the client such that it is ready for a new request.
+        
+        ***********************************************************************/
+
+        void reset ()
+        {
+                headersIn.reset;
+                headersOut.reset;
+                paramsOut.reset;
+        }
+
+        /***********************************************************************
+
+                enable/disable the internal redirection suppport
+        
+        ***********************************************************************/
+
+        void enableRedirect (bool yes)
+        {
+                doRedirect = yes;
+        }
+
+        /***********************************************************************
+
+                set timeout period for read operation
+        
+        ***********************************************************************/
+
+        void setTimeout (TimeSpan interval)
+        {
+                timeout = interval;
+        }
+
+        /**
+         * Deprecated: use setTimeout(TimeSpan) instead
+         */
+        deprecated void setTimeout(double interval)
+        {
+                setTimeout(TimeSpan.interval(interval));
+        }
+
+        /***********************************************************************
+
+                Overridable method to create a Socket. You may find a need 
+                to override this for some purpose; perhaps to add input or 
+                output filters.
+                 
+        ***********************************************************************/
+
+        protected SocketConduit createSocket ()
+        {
+                return new SocketConduit;
+        }
+
+        /***********************************************************************
+        
+                Make a request for the resource specified via the constructor,
+                using the specified timeout period (in milli-seconds).The 
+                return value represents the input buffer, from which all
+                returned headers and content may be accessed.
+                
+        ***********************************************************************/
+
+        IBuffer open (IBuffer buffer = null)
+        {
+                return open (method, null, buffer);
+        }
+
+        /***********************************************************************
+        
+                Make a request for the resource specified via the constructor,
+                using a callback for pumping additional data to the host. This 
+                defaults to a three-second timeout period. The return value 
+                represents the input buffer, from which all returned headers 
+                and content may be accessed.
+                
+        ***********************************************************************/
+
+        IBuffer open (Pump pump, IBuffer buffer = null)
+        {
+                return open (method, pump, buffer);
+        }
+
+        /***********************************************************************
+        
+                Make a request for the resource specified via the constructor
+                using the specified timeout period (in micro-seconds), and a
+                user-defined callback for pumping additional data to the host.
+                The callback would be used when uploading data during a 'put'
+                operation (or equivalent). The return value represents the 
+                input buffer, from which all returned headers and content may 
+                be accessed.
+
+                Note that certain request-headers may generated automatically
+                if they are not present. These include a Host header and, in
+                the case of Post, both ContentType & ContentLength for a query
+                type of request. The latter two are *not* produced for Post
+                requests with 'pump' specified ~ when using 'pump' to output
+                additional content, you must explicitly set your own headers.
+
+                Note also that IOException instances may be thrown. These 
+                should be caught by the client to ensure a close() operation
+                is always performed
+                
+        ***********************************************************************/
+
+        private IBuffer open (RequestMethod method, Pump pump, IBuffer input)
+        {
+                // create socket, and connect it 
+                socket = createSocket;
+                socket.setTimeout (timeout);
+                socket.connect (address);
+
+                // create buffers for input and output
+                if (input)
+                    input.clear, input.setConduit (socket);
+                else
+                   input = new Buffer (socket);
+                output = new Buffer (socket);
+
+                // save for read() method
+                this.input = input;
+
+                // setup a Host header
+                if (headersOut.get (HttpHeader.Host, null) is null)
+                    headersOut.add (HttpHeader.Host, uri.getHost);
+
+                // http/1.0 needs connection:close
+                headersOut.add (HttpHeader.Connection, "close");
+
+                // attach/extend query parameters if user has added some
+                tmp.clear;
+                auto query = uri.extendQuery (paramsOut.formatTokens(tmp, "&"));
+
+                // patch request path?
+                auto path = uri.getPath;
+                if (path.length is 0)
+                    path = "/";
+ 
+                // format encoded request 
+                output (method.name) (" "), uri.encode (&output.consume, path, uri.IncPath);
+
+                // should we emit query as part of request line?
+                if (query.length)
+                    if (method is Get)
+                        output ("?"), uri.encode (&output.consume, query, uri.IncQueryAll);
+                    else 
+                       if (method is Post && pump.funcptr is null)
+                          {
+                          // we're POSTing query text - add default info
+                          if (headersOut.get (HttpHeader.ContentType, null) is null)
+                              headersOut.add (HttpHeader.ContentType, "application/x-www-form-urlencoded");
+
+                          if (headersOut.get (HttpHeader.ContentLength, null) is null)
+                              headersOut.addInt (HttpHeader.ContentLength, query.length);
+                          }
+
+                // complete the request line, and emit headers too
+                output (" ") (DefaultHttpVersion) (HttpConst.Eol);
+   
+                headersOut.produce (&output.consume, HttpConst.Eol);
+                output (HttpConst.Eol);
+
+                // user has additional data to send?
+                if (pump.funcptr)
+                    pump (output);
+                else
+                   // send encoded POST query instead?
+                   if (method is Post && query.length)
+                       uri.encode (&output.consume, query, uri.IncQueryAll);
+
+                // send entire request
+                output.flush;
+
+                // Token for initial parsing of input header lines
+                auto line = new LineIterator!(char) (input);
+
+                // skip any blank lines
+                while (line.next && line.get.length is 0) 
+                      {}
+
+                // throw if we experienced a timeout
+                if (socket.hadTimeout)
+                    responseLine.error ("response timeout");
+
+                // is this a bogus request?
+                if (line.get.length is 0)
+                    responseLine.error ("truncated response");
+
+                // read response line
+                responseLine.parse (line.get);
+
+                // parse incoming headers
+                headersIn.reset.parse (input);
+
+                // check for redirection
+                if (doRedirect)
+                    switch (responseLine.getStatus)
+                           {
+                           case HttpResponseCode.SeeOther:
+                           case HttpResponseCode.MovedPermanently:
+                           case HttpResponseCode.MovedTemporarily:
+                           case HttpResponseCode.TemporaryRedirect:
+                                // drop this connection
+                                close;
+   
+                                // remove any existing Host header
+                                headersOut.remove (HttpHeader.Host);
+
+                                // parse redirected uri
+                                auto redirect = headersIn.get (HttpHeader.Location, "[missing url]");
+                                uri.relParse (redirect);
+
+                                // decode the host name (may take a second or two)
+                                auto host = uri.getHost();
+                                if (host)
+                                    address = new InternetAddress (uri.getHost, uri.getValidPort);
+                                else
+                                   responseLine.error ("redirect has invalid url: "~redirect);
+
+                                // figure out what to do
+                                if (method is Get || method is Head)
+                                    return open (method, pump, input);
+                                else
+                                   if (method is Post)
+                                       return redirectPost (pump, input, responseLine.getStatus);
+                                   else
+                                      responseLine.error ("unexpected redirect for method "~method.name);
+                           default:
+                                break;
+                           }
+
+                // return the input buffer
+                return input;
+        }
+
+        /***********************************************************************
+        
+                Read the content from the returning input stream, up to a
+                maximum length, and pass content to the given sink delegate
+                as it arrives. 
+
+                Exits when length bytes have been processed, or an Eof is
+                seen on the stream.
+
+        ***********************************************************************/
+
+        void read (void delegate (void[]) sink, long len = long.max)
+        {
+                while (len > 0)
+                      {
+                      auto content = input.slice;
+                      if ((len -= content.length) < 0)
+                         {
+                         content = content [0 .. $ + cast(size_t) len];
+                         sink (content);
+                         input.skip (content.length);
+                         }
+                      else
+                         {
+                         sink (content);
+                         input.clear;
+                         if (input.fill(input.input) is socket.Eof)
+                             break;
+                         }
+                      }
+        }
+
+        /***********************************************************************
+        
+                Handle redirection of Post
+                
+                Guidance for the default behaviour came from this page: 
+                http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
+
+        ***********************************************************************/
+
+        IBuffer redirectPost (Pump pump, IBuffer input, int status)
+        {
+                switch (status)
+                       {
+                            // use Get method to complete the Post
+                       case HttpResponseCode.SeeOther:
+                       case HttpResponseCode.MovedTemporarily:
+
+                            // remove POST headers first!
+                            headersOut.remove (HttpHeader.ContentLength);
+                            headersOut.remove (HttpHeader.ContentType);
+                            paramsOut.reset;
+                            return open (Get, null, input);
+
+                            // try entire Post again, if user say OK
+                       case HttpResponseCode.MovedPermanently:
+                       case HttpResponseCode.TemporaryRedirect:
+                            if (canRepost (status))
+                                return open (this.method, pump, input);
+                            // fall through!
+
+                       default:
+                            responseLine.error ("Illegal redirection of Post");
+                       }
+                return null;
+        }
+
+        /***********************************************************************
+        
+                Handle user-notification of Post redirection. This should
+                be overridden appropriately.
+
+                Guidance for the default behaviour came from this page: 
+                http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
+
+        ***********************************************************************/
+
+        bool canRepost (uint status)
+        {
+                return false;
+        }
+}
+
+
+/******************************************************************************
+
+        Class to represent an HTTP response-line
+
+******************************************************************************/
+
+private class ResponseLine : HttpTriplet
+{
+        private char[]          vers,
+                                reason;
+        private int             status;
+
+        /**********************************************************************
+
+                test the validity of these tokens
+
+        **********************************************************************/
+
+        void test ()
+        {
+                vers = tokens[0];
+                reason = tokens[2];
+                status = cast(int) Integer.convert (tokens[1]);
+                if (status is 0)
+                   {
+                   status = cast(int) Integer.convert (tokens[2]);
+                   if (status is 0)
+                       error ("Invalid HTTP response: '"~tokens[0]~"' '"~tokens[1]~"' '" ~tokens[2] ~"'");
+                   }
+        }
+
+        /**********************************************************************
+
+                Return HTTP version
+
+        **********************************************************************/
+
+        char[] getVersion ()
+        {
+                return vers;
+        }
+
+        /**********************************************************************
+
+                Return reason text
+
+        **********************************************************************/
+
+        char[] getReason ()
+        {
+                return reason;
+        }
+
+        /**********************************************************************
+
+                Return status integer
+
+        **********************************************************************/
+
+        int getStatus ()
+        {
+                return status;
+        }
+}