diff --git a/CHANGES b/CHANGES
new file mode 100644
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,133 @@
+Version 4000.4.0: release 2022-02-22
+ * Restrict to GHC >=7.6 and associated cleanups (Andreas Abel)
+ * Drop deprecated instance Error (Andreas Abel)
+ * Preparation for mtl-2.3 (Andreas Abel)
+ * General tidying (Andreas Abel)
+ * Support GHC 9.2 (Bodigrim)
+
+Version 4000.3.16: release 2021-03-20
+ * Support GHC-9.0 (Oleg Genrus)
+ * Various dependency bumps (multiple people)
+ * Try all addresses returned by getAddrInfo (Fraser Tweedale)
+
+Version ?
+
+ * If the URI contains "user:pass@" part, use it for Basic Authorization
+ * Add a test harness.
+ * Don't leak a socket when getHostAddr throws an exception.
+ * Send cookies in request format, not response format.
+ * Moved BrowserAction to be a StateT IO, with instances for
+   Applicative, MonadIO, MonadState.
+ * Add method to control size of connection pool.
+ * Consider both host and port when reusing connections.
+ * Handle response code 304 "not modified" properly.
+ * Fix digest authentication by fixing md5 output string rep.
+ * Make the default user agent string follow the package version.
+ * Document lack of HTTPS support and fail when clients try
+   to use it instead of silently falling back to HTTP.
+ * Add helper to set the request type and body.
+
+Version 4000.1.2: release 2011-08-11
+ * Turn off buffering for the debug log.
+ * Update installation instructions.
+ * Bump base dependency to support GHC 7.2.
+
+Version 4000.1.1: release 2010-11-28
+ * Be tolerant of LF (instead of CRLF which is the spec) in responses.
+
+Version 4000.1.0: release 2010-11-09
+ * Retroactively fixed CHANGES to refer to 4000.x.x instead of
+   4004.x.x.
+ * Fix problem with close looping on certain URLs due to trying
+   to munch the rest of the stream even on EOF. Modified from
+   a fix by Daniel Wagner.
+    * This involves a new class member for HStream and is thus an
+      API change, but one that will only affect clients that
+      define their own payload type to replace String/ByteString.
+ * Applied patch by Antoine Latter to fix problem with 301 and 307
+   redirects.
+
+Version 4000.0.10: release 2010-10-29
+ * Bump base dependency to support GHC 7.0.
+ * Stop using 'fail' from the Either monad and instead build Left
+   values explicitly; the behaviour of fail is changing in GHC 7.0
+   and this avoids being sensitive to the change.
+
+Version 4000.0.9: release 2009-12-20
+
+ * Export headerMap from Network.HTTP.Headers
+   (suggested by David Leuschner.)
+ * Fix Network.TCP.{isTCPConnectedTo,isConnectedTo} to be useful.
+ * Always delay closing non-persistent connections until we reach EOF.
+   Delaying it until then is vital when reading the response out as a 
+   lazy ByteString; all of the I/O may not have happened by the time we
+   were returning the HTTP response. Bug manifested itself occasionally
+   with larger responses. Courtesy of Valery Vorotyntsev; both untiring bug
+   hunt and fix.
+ * drop unused type argument from Network.Browser.BrowserEvent; needlessly general.
+   (patch provided by Daniel Wagner.)
+
+Version 4000.0.8: release 2009-08-05
+
+ * Incorporated proxy setting lookup and parsing contribution
+   by Eric Kow; provided in Network.HTTP.Proxy
+ * Factor out HTTP Cookies and Auth handling into separate
+   modules Network.HTTP.Cookie, Network.HTTP.Auth
+ * new Network.Browser functionality for hooking up the
+   proxy detection code in Network.HTTP.Proxy:
+
+      setCheckForProxy :: Bool -> BrowserAction t ()
+      getCheckForProxy :: BrowserAction t Bool
+
+   If you do 'setCheckForProxy True' within a browser
+   session, the proxy-checking code will be called upon.
+   Use 'getCheckForProxy' to get the current setting for
+   this flag.
+
+ * Network.Browser: if HTTP Basic Auth is allowed and
+   server doesn't 401-challenge with an WWW-Authenticate:
+   header, simply assume / realm and proceed. Preferable
+   than failing, even if server is the wrong.
+
+Version 4000.0.7: release 2009-05-22
+
+ * Minor release.
+ * Added 
+     Network.TCP.openSocketStream :: (BufferType t)
+                                  => String {-host-}
+				  -> Socket
+				  -> IO (HandleStream t)
+
+   for interfacing to pre-existing @Socket@s. Contributed and
+   suggested by <http://trac.haskell.org/http/ticket/7>.
+
+Version 4000.0.6: release 2009-04-21; changes from 4000.0.5
+
+ * Network.Browser: use HTTP.HandleStream.sendHTTP_notify, not HTTP.sendHTTP_notify
+   when issuing requests. The latter runs the risk of undoing request normalization.
+ * Network.HTTP.Base.normalizeRequest: when normalizing proxy-bound requests,
+   insert a Host: header if none present. Set it to the destination server authority,
+   not the proxy.
+ * Network.Browser: don't fail on seeing invalid cookie values, but report them
+   as errors and continue.
+
+Version 4000.0.5: release 2009-03-30; changes from 4000.0.4
+
+ * Get serious about comments and Haddock documentation.
+ * Cleaned up normalization of requests, fixing bugs and bringing together
+   previous disparate attempts at handling this.
+ * RequestMethod now supports custom verbs; use the (Custom String) constructor
+ * Beef up Network.HTTP.Base's support for normalizing requests and URIs:
+ 
+    * added splitRequestURI which divides a URI into two; the Authority portion
+      (as a String) and the input URI sans the authority portion. Useful when
+      wanting to split up a request's URI into its Host: and abs_path pieces.
+    * added normalizeRequest :: Bool -> Request ty -> Request ty, which 
+      fixes up a requests URI path and Host: info depending on whether it is
+      destined for a proxy or not (controlled by the Bool.)
+    * moved defaultRequest, defaultRequest_, libUA from Network.Browser
+      to Network.HTTP.Base
+    * added mkRequest :: RequestMethod -> URI -> Bool -> Request ty
+      for constructing normalized&sane Request bases on top of which
+      you can add custom headers, body payload etc.
+
diff --git a/HTTP.cabal b/HTTP.cabal
--- a/HTTP.cabal
+++ b/HTTP.cabal
@@ -1,26 +1,184 @@
+Cabal-Version: 3.0
 Name: HTTP
-Version: 3000.0.0
-License: BSD3
+Version: 4000.5.0
+Build-type: Simple
+License: BSD-3-Clause
 License-file: LICENSE
-Copyright: 
-  Copyright (c) 2002, Warrick Gray
-  Copyright (c) 2002-2005, Ian Lynagh
-  Copyright (c) 2003-2006, Bjorn Bringert
-  Copyright (c) 2004, Andre Furtado
-  Copyright (c) 2004, Ganesh Sittampalam
-  Copyright (c) 2004-2005, Dominic Steinitz
 Author: Warrick Gray <warrick.gray@hotmail.com>
-Maintainer: Bjorn Bringert <bjorn@bringert.net>
-Homepage: http://www.haskell.org/http/
-Description: A library for client-side HTTP
-Build-depends: base, network, parsec
-Exposed-modules: 
+Maintainer: Ganesh Sittampalam <ganesh@earth.li>
+Homepage: https://github.com/haskell/HTTP
+Category: Network
+Synopsis: A library for client-side HTTP
+Description:
+
+ The HTTP package supports client-side web programming in Haskell. It lets you set up
+ HTTP connections, transmitting requests and processing the responses coming back, all
+ from within the comforts of Haskell. It's dependent on the network package to operate,
+ but other than that, the implementation is all written in Haskell.
+ .
+ A basic API for issuing single HTTP requests + receiving responses is provided. On top
+ of that, a session-level abstraction is also on offer  (the @BrowserAction@ monad);
+ it taking care of handling the management of persistent connections, proxies,
+ state (cookies) and authentication credentials required to handle multi-step
+ interactions with a web server.
+ .
+ The representation of the bytes flowing across is extensible via the use of a type class,
+ letting you pick the representation of requests and responses that best fits your use.
+ Some pre-packaged, common instances are provided for you (@ByteString@, @String@).
+ .
+ Here's an example use:
+ .
+ >
+ >    do
+ >      rsp <- Network.HTTP.simpleHTTP (getRequest "http://www.haskell.org/")
+ >              -- fetch document and return it (as a 'String'.)
+ >      fmap (take 100) (getResponseBody rsp)
+ >
+ >    do
+ >      (_, rsp)
+ >         <- Network.Browser.browse $ do
+ >               setAllowRedirects True -- handle HTTP redirects
+ >               request $ getRequest "http://www.haskell.org/"
+ >      return (take 100 (rspBody rsp))
+ .
+ __Note:__ This package does not support HTTPS connections.
+ If you need HTTPS, take a look at the following packages:
+ .
+ * <http://hackage.haskell.org/package/http-streams http-streams>
+ .
+ * <http://hackage.haskell.org/package/http-client http-client> (in combination with
+ <http://hackage.haskell.org/package/http-client-tls http-client-tls>)
+ .
+ * <http://hackage.haskell.org/package/req req>
+ .
+ * <http://hackage.haskell.org/package/wreq wreq>
+ .
+
+Extra-Source-Files: CHANGES
+
+tested-with:
+  GHC == 9.12.2
+  GHC == 9.10.3
+  GHC == 9.8.4
+  GHC == 9.6.7
+  GHC == 9.4.8
+  GHC == 9.2.8
+  GHC == 9.0.2
+  GHC == 8.10.7
+  GHC == 8.8.4
+  GHC == 8.6.5
+  GHC == 8.4.4
+  GHC == 8.2.2
+  -- CI failing for GHC 8.0 because of https://github.com/haskell/cabal/issues/10379
+  -- GHC == 8.0.2
+
+Source-Repository head
+  type: git
+  location: https://github.com/haskell/HTTP.git
+
+Flag warn-as-error
+  default:     False
+  description: Build with warnings-as-errors
+  manual:      True
+
+Flag conduit10
+  description: Use version 1.0.x or below of the conduit package (for the test suite)
+  default: False
+
+Flag warp-tests
+  description: Test against warp
+  default:     False
+  manual:      True
+
+Library
+  Autogen-modules: Paths_HTTP
+  Exposed-modules:
+                 Network.BufferType,
                  Network.Stream,
-                 Network.TCP,                
-		 Network.HTTP,
+                 Network.StreamDebugger,
+                 Network.StreamSocket,
+                 Network.TCP,
+                 Network.HTTP,
+                 Network.HTTP.Headers,
+                 Network.HTTP.Base,
+                 Network.HTTP.Stream,
+                 Network.HTTP.Auth,
+                 Network.HTTP.Cookie,
+                 Network.HTTP.Proxy,
+                 Network.HTTP.HandleStream,
                  Network.Browser
-Other-modules:
+  Other-modules:
                  Network.HTTP.Base64,
-                 Network.HTTP.MD5,
-                 Network.HTTP.MD5Aux
-GHC-options: -O -fwarn-missing-signatures
+                 Network.HTTP.MD5Aux,
+                 Network.HTTP.Utils,
+                 Paths_HTTP
+  GHC-options: -fwarn-missing-signatures -Wall
+
+  -- note the test harness constraints should be kept in sync with these
+  -- where dependencies are shared
+  build-depends:
+      base          >= 4.6.0.0   && < 4.22
+    , array         >= 0.3.0.2   && < 0.6
+    , bytestring    >= 0.9.1.5   && < 0.13
+    , parsec        >= 2.0       && < 3.2
+    , time          >= 1.1.2.3   && < 1.15
+    , transformers  >= 0.2.0.0   && < 0.7
+        -- transformers-0.2.0.0 is the first to have Control.Monad.IO.Class
+    -- The following dependencies are refined by flags, but they should
+    -- still be mentioned here on the top-level.
+    , mtl           >= 2.0.0.0   && < 2.4
+    , network       >= 2.6       && < 3.3
+    , network-uri   >= 2.6       && < 2.7
+
+  default-language: Haskell98
+  default-extensions: FlexibleInstances
+
+  if flag(warn-as-error)
+    ghc-options:      -Werror
+
+  if os(windows)
+    Build-depends: Win32 >= 2.2.0.0 && < 2.15
+
+Test-Suite test
+  type: exitcode-stdio-1.0
+
+  default-language: Haskell98
+  hs-source-dirs: test
+  main-is: httpTests.hs
+
+  other-modules:
+    Httpd
+    UnitTests
+
+  ghc-options: -Wall
+
+  build-depends:
+      HTTP
+    -- constraints inherited from HTTP
+    , base
+    , bytestring
+    , mtl
+    , network
+    , network-uri
+    -- extra dependencies
+    , deepseq               >= 1.3.0.0  && < 1.6
+    , httpd-shed            >= 0.4      && < 0.5
+    , HUnit                 >= 1.2.0.1  && < 1.7
+    , pureMD5               >= 0.2.4    && < 2.2
+    , split                 >= 0.1.3    && < 0.3
+    , test-framework        >= 0.2.0    && < 0.9
+    , test-framework-hunit  >= 0.3.0    && < 0.4
+
+  if flag(warp-tests)
+    CPP-Options: -DWARP_TESTS
+    build-depends:
+        case-insensitive    >= 0.4.0.1  && < 1.3
+      , conduit             >= 1.0.8    && < 1.4
+      , http-types          >= 0.8.0    && < 1.0
+      , wai                 >= 2.1.0    && < 3.3
+      , warp                >= 2.1.0    && < 3.5
+
+    if flag(conduit10)
+      build-depends: conduit < 1.1
+    else
+      build-depends: conduit >= 1.1, conduit-extra >= 1.1 && < 1.4
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -2,8 +2,18 @@
 Copyright (c) 2002-2005, Ian Lynagh
 Copyright (c) 2003-2006, Bjorn Bringert
 Copyright (c) 2004, Andre Furtado
-Copyright (c) 2004, Ganesh Sittampalam
 Copyright (c) 2004-2005, Dominic Steinitz
+Copyright (c) 2007, Robin Bate Boerop
+Copyright (c) 2008-2010, Sigbjorn Finne
+Copyright (c) 2009, Eric Kow
+Copyright (c) 2010, Antoine Latter
+Copyright (c) 2004, 2010-2011, Ganesh Sittampalam
+Copyright (c) 2011, Duncan Coutts
+Copyright (c) 2011, Matthew Gruen
+Copyright (c) 2011, Jeremy Yallop
+Copyright (c) 2011, Eric Hesselink
+Copyright (c) 2011, Yi Huang
+Copyright (c) 2011, Tom Lokhorst
 
 All rights reserved.
 
diff --git a/Network/Browser.hs b/Network/Browser.hs
--- a/Network/Browser.hs
+++ b/Network/Browser.hs
@@ -1,976 +1,1091 @@
------------------------------------------------------------------------------
--- |
--- Module      :  Network.Browser
--- Copyright   :  (c) Warrick Gray 2002
--- License     :  BSD
--- 
--- Maintainer  :  bjorn@bringert.net
--- Stability   :  experimental
--- Portability :  non-portable (not tested)
---
--- An HTTP\/1.1 compatible wrapper for the HTTP module.
------------------------------------------------------------------------------
- 
-{-
-
-  Change Log:
-   - altered 'closeTCP' to 'close', for consistency with altered HTTP
-   - added debugging settings to browser.
-
-  To Do: 
-   - testing!!!
-   - remove BrowserAction type? Possibly replace with IORef?
-   - (more todo's in the HTTP mod)
-
--}
-
-module Network.Browser (
-    BrowserState,
-    BrowserAction,      -- browser monad, effectively a state monad.
-    Cookie,
-    Form(..),
-    Proxy(..),
-    
-    browse,             -- BrowserAction a -> IO a
-    request,            -- Request -> BrowserAction Response
-    
-    setAllowRedirects,
-    getAllowRedirects,
-    
-    Authority(..),
-    getAuthorities, 
-    setAuthorities, 
-    addAuthority, 
-    getAuthorityGen, 
-    setAuthorityGen, 
-    setAllowBasicAuth,
-
-    setCookieFilter,
-    defaultCookieFilter,
-    userCookieFilter,
-    
-    getCookies,
-    setCookies,
-    addCookie,
-
-    setErrHandler,
-    setOutHandler,
-
-    setProxy,
-
-    setDebugLog,
-
-    out,
-    err,
-    ioAction,           -- :: IO a -> BrowserAction a
-
-    defaultGETRequest,
-    formToRequest,
-    uriDefaultTo,
-    uriTrimHost
-) where
-
-import Network.HTTP
-
-import Data.Char (toLower,isAlphaNum,isSpace)
-import Data.List (isPrefixOf,isSuffixOf,elemIndex,elemIndices)
-import Data.Maybe
-import Control.Monad (foldM,filterM,liftM,when)
-import Text.ParserCombinators.Parsec
-import Network.URI
-
-import qualified System.IO
-import Data.Word (Word8)
-import qualified Network.HTTP.MD5 as MD5
-import qualified Network.HTTP.Base64 as Base64
-
-type Octet = Word8
-
-------------------------------------------------------------------
------------------------ Miscellaneous ----------------------------
-------------------------------------------------------------------
-
-word, quotedstring :: Parser String
-quotedstring =
-    do { char '"'
-       ; str <- many (satisfy $ not . (=='"'))
-       ; char '"'
-       ; return str
-       }
-
-word = many1 (satisfy (\x -> isAlphaNum x || x=='_' || x=='.' || x=='-' || x==':'))
-
--- misc string fns
-trim :: String -> String
-trim = let dropspace = dropWhile isSpace in
-       reverse . dropspace . reverse . dropspace
-
-
--- | Splits a list into two parts, the delimiter occurs
--- at the head of the second list.  Nothing is returned
--- when no occurance of the delimiter is found.
-split :: Eq a => a -> [a] -> Maybe ([a],[a])
-split delim list = case delim `elemIndex` list of
-    Nothing -> Nothing
-    Just x  -> Just $ splitAt x list
-    
-
-
--- | Removes delimiters.
-splitMany :: Eq a => a -> [a] -> [[a]]
-splitMany delim str = fn str ixs
-    where
-        ixs = elemIndices delim str
-        fn _ [] = []
-        fn ls (h:t) = let (a,b) = splitAt h ls in a : fn b t
-
-
--- | Returns a URI that is consistent with the first
--- argument uri when read in the context of a second.
--- If second argument is not sufficient context for
--- determining a full URI then anarchy reins.
-uriDefaultTo :: URI -> URI -> URI
-uriDefaultTo a b =
-    case a `relativeTo` b of
-        Nothing -> a
-        Just x  -> x
-
-
-uriTrimHost :: URI -> URI
-uriTrimHost uri = uri { uriScheme="", uriAuthority=Nothing }
-
-
-------------------------------------------------------------------
------------------------ Cookie Stuff -----------------------------
-------------------------------------------------------------------
-
--- Some conventions: 
---     assume ckDomain is lowercase
---
-data Cookie = MkCookie { ckDomain
-                       , ckName
-                       , ckValue :: String
-                       , ckPath
-                       , ckComment
-                       , ckVersion :: Maybe String
-                       }
-    deriving(Show,Read)
-
-
-instance Eq Cookie where
-    a == b  =  ckDomain a == ckDomain b 
-            && ckName a == ckName b 
-            && ckPath a == ckPath b
-
-
-
-defaultCookieFilter :: URI -> Cookie -> IO Bool
-defaultCookieFilter url cky = return True
-
-userCookieFilter :: URI -> Cookie -> IO Bool
-userCookieFilter url cky =
-    do putStrLn ("Set-Cookie received when requesting: " ++ show url)
-       case ckComment cky of
-          Nothing -> return ()
-          Just x  -> putStrLn ("Cookie Comment:\n" ++ x)
-       putStrLn ("Domain/Path: " ++ ckDomain cky ++ 
-            case ckPath cky of
-                Nothing -> ""
-                Just x  -> "/" ++ x)
-       putStrLn (ckName cky ++ '=' : ckValue cky)
-       System.IO.hSetBuffering System.IO.stdout System.IO.NoBuffering
-       System.IO.hSetBuffering System.IO.stdin System.IO.NoBuffering
-       System.IO.hPutStr System.IO.stdout "Accept [y/n]? "
-       x <- System.IO.hGetChar System.IO.stdin
-       System.IO.hSetBuffering System.IO.stdin System.IO.LineBuffering
-       System.IO.hSetBuffering System.IO.stdout System.IO.LineBuffering
-       return (toLower x == 'y')
-       
-
-
--- | Serialise a Cookie for inclusion in a request.
-cookieToHeader :: Cookie -> Header
-cookieToHeader ck = Header HdrCookie text
-    where
-        text = "$Version=" ++ fromMaybe "0" (ckVersion ck)
-             ++ ';' : ckName ck ++ "=" ++ ckValue ck
-             ++ (case ckPath ck of
-                     Nothing -> ""
-                     Just x  -> ";$Path=" ++ x)
-             ++ ";$Domain=" ++ ckDomain ck
-
-
-
-{- replace "error" call with [] in final version? -}
-headerToCookies :: String -> Header -> [Cookie]
-headerToCookies dom (Header HdrSetCookie val) = 
-    case parse cookies "" val of
-        Left e  -> error ("Cookie parse failure on: " ++ val ++ " " ++ show e)
-        Right x  -> x
-    where
-        cookies :: Parser [Cookie]
-        cookies = sepBy1 cookie (char ',')
-
-        cookie :: Parser Cookie
-        cookie =
-            do { name <- word
-               ; spaces
-               ; char '='
-               ; spaces
-               ; val <- cvalue
-               ; args <- cdetail
-               ; return $ mkCookie name val args
-               }
-
-        cvalue :: Parser String
-        
-        spaces = many (satisfy isSpace)
-
-        cvalue = quotedstring <|> many1 (satisfy $ not . (==';'))
-       
-        -- all keys in the result list MUST be in lower case
-        cdetail :: Parser [(String,String)]
-        cdetail = many $
-            try (do { spaces
-               ; char ';'
-               ; spaces
-               ; s1 <- word
-               ; spaces
-               ; s2 <- option "" (do { char '=' ; spaces ; v <- cvalue ; return v })
-               ; return (map toLower s1,s2)
-               })
-
-        mkCookie :: String -> String -> [(String,String)] -> Cookie
-        mkCookie nm val more = MkCookie { ckName=nm
-                                        , ckValue=val
-                                        , ckDomain=map toLower (fromMaybe dom (lookup "domain" more))
-                                        , ckPath=lookup "path" more
-                                        , ckVersion=lookup "version" more
-                                        , ckComment=lookup "comment" more
-                                        }
-
-      
-
--- | Adds a cookie to the browser state, removing duplicates.
-addCookie :: Cookie -> BrowserAction ()
-addCookie c = alterBS (\b -> b { bsCookies=c : fn (bsCookies b) })
-    where
-        fn = filter (not . (==c))
-
-setCookies :: [Cookie] -> BrowserAction ()
-setCookies cs = alterBS (\b -> b { bsCookies=cs })
-
-getCookies :: BrowserAction [Cookie]
-getCookies = getBS bsCookies
-
--- ...get domain specific cookies...
--- ... this needs changing for consistency with rfc2109...
--- ... currently too broad.
-getCookiesFor :: String -> String -> BrowserAction [Cookie]
-getCookiesFor dom path =
-    do cks <- getCookies
-       return (filter cookiematch cks)
-    where
-        cookiematch :: Cookie -> Bool
-        cookiematch ck = ckDomain ck `isSuffixOf` dom
-                      && case ckPath ck of
-                             Nothing -> True
-                             Just p  -> p `isPrefixOf` path
-      
-
-setCookieFilter :: (URI -> Cookie -> IO Bool) -> BrowserAction ()
-setCookieFilter f = alterBS (\b -> b { bsCookieFilter=f })
-
-getCookieFilter :: BrowserAction (URI -> Cookie -> IO Bool)
-getCookieFilter = getBS bsCookieFilter
-
-------------------------------------------------------------------
------------------------ Authorisation Stuff ----------------------
-------------------------------------------------------------------
-
-{-
-
-The browser handles 401 responses in the following manner:
-  1) extract all WWW-Authenticate headers from a 401 response
-  2) rewrite each as a Challenge object, using "headerToChallenge"
-  3) pick a challenge to respond to, usually the strongest
-     challenge understood by the client, using "pickChallenge"
-  4) generate a username/password combination using the browsers
-     "bsAuthorityGen" function (the default behaviour is to ask
-     the user)
-  5) build an Authority object based upon the challenge and user
-     data, store this new Authority in the browser state
-  6) convert the Authority to a request header and add this
-     to a request using "withAuthority"
-  7) send the amended request
-
-Note that by default requests are annotated with authority headers
-before the first sending, based upon previously generated Authority
-objects (which contain domain information).  Once a specific authority
-is added to a rejected request this predictive annotation is suppressed.
-
-407 responses are handled in a similar manner, except
-   a) Authorities are not collected, only a single proxy authority
-      is kept by the browser
-   b) If the proxy used by the browser (type Proxy) is NoProxy, then
-      a 407 response will generate output on the "err" stream and
-      the response will be returned.
-
-
-Notes:
- - digest authentication so far ignores qop, so fails to authenticate 
-   properly with qop=auth-int challenges
- - calculates a1 more than necessary
- - doesn't reverse authenticate
- - doesn't properly receive AuthenticationInfo headers, so fails
-   to use next-nonce etc
-
--}
-
-
-data Algorithm = AlgMD5 | AlgMD5sess
-    deriving(Eq)
-
-instance Show Algorithm where
-    show AlgMD5 = "md5"
-    show AlgMD5sess = "md5-sess"
-
-
-data Qop = QopAuth | QopAuthInt
-    deriving(Eq,Show)
-
-
-data Challenge = ChalBasic  { chRealm   :: String }
-               | ChalDigest { chRealm   :: String
-                            , chDomain  :: [URI]
-                            , chNonce   :: String
-                            , chOpaque  :: Maybe String
-                            , chStale   :: Bool
-                            , chAlgorithm ::Maybe Algorithm
-                            , chQop     :: [Qop]
-                            }
-
--- | Convert WWW-Authenticate header into a Challenge object
-headerToChallenge :: URI -> Header -> Maybe Challenge
-headerToChallenge baseURI (Header _ str) =
-    case parse challenge "" str of
-        Left e -> Nothing
-        Right (name,props) -> case name of
-            "basic"  -> mkBasic props
-            "digest" -> mkDigest props
-            _        -> Nothing
-    where
-        challenge :: Parser (String,[(String,String)])
-        challenge =
-            do { nme <- word
-               ; pps <- cprops
-               ; return (map toLower nme,pps)
-               }
-
-        cprops = sepBy1 cprop comma
-
-        comma = do { spaces ; char ',' ; spaces }
-
-        cprop =
-            do { nm <- word
-               ; char '='
-               ; val <- quotedstring
-               ; return (map toLower nm,val)
-               }
-
-        quotedstring =
-            do { char '"'
-               ; str <- many (satisfy (not.(=='"')))
-               ; char '"'
-               ; return str
-               }        
-        
-        mkBasic, mkDigest :: [(String,String)] -> Maybe Challenge
-
-        mkBasic params = fmap ChalBasic (lookup "realm" params)
-
-        mkDigest params =
-            -- with Maybe monad
-            do { r <- lookup "realm" params
-               ; n <- lookup "nonce" params
-               ; return $ 
-                    ChalDigest { chRealm  = r
-                               , chDomain = (annotateURIs 
-                                            $ map parseURI
-                                            $ words 
-                                            $ fromMaybe [] 
-                                            $ lookup "domain" params)
-                               , chNonce  = n
-                               , chOpaque = lookup "opaque" params
-                               , chStale  = "true" == (map toLower
-                                           $ fromMaybe "" (lookup "stale" params))
-                               , chAlgorithm= readAlgorithm (fromMaybe "MD5" $ lookup "algorithm" params)
-                               , chQop    = readQop (fromMaybe "" $ lookup "qop" params)
-                               }
-               }
-
-        annotateURIs :: [Maybe URI] -> [URI]
-        annotateURIs = (map (\u -> fromMaybe u (u `relativeTo` baseURI))) . catMaybes
-
-        -- Change These:
-        readQop :: String -> [Qop]
-        readQop = catMaybes . (map strToQop) . (splitMany ',')
-
-        strToQop str = case map toLower (trim str) of
-            "auth"     -> Just QopAuth
-            "auth-int" -> Just QopAuthInt
-            _          -> Nothing
-
-        readAlgorithm str = case map toLower (trim str) of
-            "md5"      -> Just AlgMD5
-            "md5-sess" -> Just AlgMD5sess
-            _          -> Nothing
-
-
-data Authority = AuthBasic { auRealm    :: String
-                           , auUsername :: String
-                           , auPassword :: String
-                           , auSite     :: URI
-                           }
-               | AuthDigest { auRealm     :: String
-                            , auUsername  :: String
-                            , auPassword  :: String
-                            , auNonce     :: String
-                            , auAlgorithm :: Maybe Algorithm
-                            , auDomain    :: [URI]
-                            , auOpaque    :: Maybe String
-                            , auQop       :: [Qop]
-                            }
-
-
--- | Return authorities for a given domain and path.
--- Assumes "dom" is lower case
-getAuthFor :: String -> String -> BrowserAction [Authority]
-getAuthFor dom pth =
-    do { list <- getAuthorities
-       ; return (filter match list)
-       }
-    where
-        match :: Authority -> Bool
-        match (AuthBasic _ _ _ s) = matchURI s
-        match (AuthDigest _ _ _ _ _ ds _ _) = or (map matchURI ds)            
-
-        matchURI :: URI -> Bool
-        matchURI s = (uriToAuthorityString s == dom) && (uriPath s `isPrefixOf` pth)
-    
-
--- | Interacting with browser state:
-getAuthorities :: BrowserAction [Authority]
-getAuthorities = getBS bsAuthorities
-
-setAuthorities :: [Authority] -> BrowserAction ()
-setAuthorities as = alterBS (\b -> b { bsAuthorities=as })
-
-addAuthority :: Authority -> BrowserAction ()
-addAuthority a = alterBS (\b -> b { bsAuthorities=a:bsAuthorities b })
-
-getAuthorityGen :: BrowserAction (URI -> String -> IO (Maybe (String,String)))
-getAuthorityGen = getBS bsAuthorityGen
-
-setAuthorityGen :: (URI -> String -> IO (Maybe (String,String))) -> BrowserAction ()
-setAuthorityGen f = alterBS (\b -> b { bsAuthorityGen=f })
-
-setAllowBasicAuth :: Bool -> BrowserAction ()
-setAllowBasicAuth ba = alterBS (\b -> b { bsAllowBasicAuth=ba })
-
-
-
-
--- TO BE CHANGED!!!
-pickChallenge :: [Challenge] -> Maybe Challenge
-pickChallenge = listToMaybe
-
-
-
--- | Retrieve a likely looking authority for a Request.
-anticipateChallenge :: Request -> BrowserAction (Maybe Authority)
-anticipateChallenge rq =
-    let uri = rqURI rq in
-    do { authlist <- getAuthFor (uriToAuthorityString uri) (uriPath uri)
-       ; return (listToMaybe authlist)
-       }
-
-
--- | Asking the user to respond to a challenge
-challengeToAuthority :: URI -> Challenge -> BrowserAction (Maybe Authority)
-challengeToAuthority uri ch =
-    -- prompt user for authority
-    if answerable ch then
-        do { prompt <- getAuthorityGen
-           ; userdetails <- ioAction $ prompt uri (chRealm ch)
-           ; case userdetails of
-               Nothing    -> return Nothing
-               Just (u,p) -> return (Just $ buildAuth ch u p)
-           }
-    else return Nothing
-    where
-        answerable :: Challenge -> Bool
-        answerable (ChalBasic _) = True
-        answerable ch            = (chAlgorithm ch) == Just AlgMD5
-
-        buildAuth :: Challenge -> String -> String -> Authority
-        buildAuth (ChalBasic r) u p = 
-            AuthBasic { auSite=uri
-                      , auRealm=r
-                      , auUsername=u
-                      , auPassword=p
-                      }
-
-         -- note to self: this is a pretty stupid operation
-         -- to perform isn't it? ChalX and AuthX are so very
-         -- similar.
-        buildAuth (ChalDigest r d n o s a q) u p =
-            AuthDigest { auRealm=r
-                       , auUsername=u
-                       , auPassword=p
-                       , auDomain=d
-                       , auNonce=n
-                       , auOpaque=o
-                       , auAlgorithm=a
-                       , auQop=q
-                       }
-
-
--- | Generating a credentials value from an Authority, in
--- the context of a specific request.  If a client nonce
--- was to be used then this function might need to
--- be of type ... -> BrowserAction String
-withAuthority :: Authority -> Request -> String
-withAuthority a rq = case a of
-        AuthBasic _ _ user pass ->
-	    "basic " ++ base64encode (auUsername a ++ ':' : auPassword a)
-        AuthDigest _ _ _ _ _ _ _ _ ->
-            "digest username=\"" ++ auUsername a 
-              ++ "\",realm=\"" ++ auRealm a
-              ++ "\",nonce=\"" ++ auNonce a
-              ++ "\",uri=\"" ++ digesturi
-              ++ ",response=\"" ++ rspdigest 
-              ++ "\""
-              -- plus optional stuff:
-              ++ ( if isJust (auAlgorithm a) then "" else ",algorithm=\"" ++ show (fromJust $ auAlgorithm a) ++ "\"" )
-              ++ ( if isJust (auOpaque a) then "" else ",opaque=\"" ++ (fromJust $ auOpaque a) ++ "\"" )
-              ++ ( if null (auQop a) then "" else ",qop=auth" )
-    where
-        rspdigest = "\"" 
-                 ++ map toLower (kd (md5 a1) (noncevalue ++ ":" ++ md5 a2))
-                 ++ "\""
-
-        -- FIXME: these probably only work right for latin-1 strings
-	stringToOctets :: String -> [Octet]
-	stringToOctets = map (fromIntegral . fromEnum)
-	octetsToString :: [Octet] -> String
-	octetsToString = map (toEnum . fromIntegral)
-	base64encode :: String -> String
-	base64encode = Base64.encode . stringToOctets
-	md5 :: String -> String
-	md5 = octetsToString . MD5.hash . stringToOctets
-
-        kd :: String -> String -> String
-        kd a b = md5 (a ++ ":" ++ b)
-
-        a1, a2 :: String
-        a1 = auUsername a ++ ":" ++ auRealm a ++ ":" ++ auPassword a
-        
-        {-
-        If the "qop" directive's value is "auth" or is unspecified, then A2
-        is:
-           A2  = Method ":" digest-uri-value
-        If the "qop" value is "auth-int", then A2 is:
-           A2  = Method ":" digest-uri-value ":" H(entity-body)
-        -}
-        a2 = show (rqMethod rq) ++ ":" ++ digesturi
-
-        digesturi = show (rqURI rq)
-        noncevalue = auNonce a
-
-
-------------------------------------------------------------------
------------------- Proxy Stuff -----------------------------------
-------------------------------------------------------------------
-
-data Proxy = NoProxy
-           | Proxy String (Maybe Authority)
-
-
-------------------------------------------------------------------
------------------- Browser State Actions -------------------------
-------------------------------------------------------------------
-
-
-data BrowserState = BS { bsErr, bsOut     :: String -> IO ()
-                       , bsCookies        :: [Cookie]
-                       , bsCookieFilter   :: URI -> Cookie -> IO Bool
-                       , bsAuthorityGen   :: URI -> String -> IO (Maybe (String,String))
-                       , bsAuthorities    :: [Authority]
-                       , bsAllowRedirects :: Bool
-                       , bsAllowBasicAuth :: Bool
-                       , bsConnectionPool :: [Connection]
-                       , bsProxy          :: Proxy
-                       , bsDebug          :: Maybe String
-                       }
-
-instance Show BrowserState where
-    show bs =  "BrowserState { " 
-            ++ show (bsCookies bs)  ++ "\n"
-           {- ++ show (bsAuthorities bs) ++ "\n"-}
-            ++ "AllowRedirects: " ++ show (bsAllowRedirects bs)
-            ++ "} "
-
-
--- Simple DIY stateful behaviour, with IO
-data BrowserAction a = BA { lift :: (BrowserState -> IO (BrowserState,a)) }
-
-instance Monad BrowserAction where
-    a >>= f  =  BA (\b -> do { (nb,v) <- lift a b ; lift (f v) nb})
-    return x =  BA (\b -> return (b,x))
-
-instance Functor BrowserAction where
-    fmap f   = liftM f
-
-
--- | Apply a browser action to a state.
-browse :: BrowserAction a -> IO a
-browse act = do x <- lift act defaultBrowserState
-                return (snd x)
-    where
-        defaultBrowserState :: BrowserState
-        defaultBrowserState = 
-            BS { bsErr              = putStrLn
-               , bsOut              = putStrLn
-               , bsCookies          = []
-               , bsCookieFilter     = defaultCookieFilter
-               , bsAuthorityGen     = (error "bsAuthGen wanted")
-               , bsAuthorities      = []
-               , bsAllowRedirects   = True
-               , bsAllowBasicAuth   = False
-               , bsConnectionPool   = []
-               , bsProxy            = NoProxy
-               , bsDebug            = Nothing 
-               }
-
--- | Alter browser state
-alterBS :: (BrowserState -> BrowserState) -> BrowserAction ()
-alterBS f = BA (\b -> return (f b,()))
-
-getBS :: (BrowserState -> a) -> BrowserAction a
-getBS f = BA (\b -> return (b,f b))
-
--- | Do an io action
-ioAction :: IO a -> BrowserAction a
-ioAction a = BA (\b -> a >>= \v -> return (b,v))
-
-
--- Stream handlers
-setErrHandler, setOutHandler :: (String -> IO ()) -> BrowserAction ()
-setErrHandler h = alterBS (\b -> b { bsErr=h })
-setOutHandler h = alterBS (\b -> b { bsOut=h })
-
-out, err :: String -> BrowserAction ()
-out s = do { f <- getBS bsOut ; ioAction $ f s }
-err s = do { f <- getBS bsErr ; ioAction $ f s }
-
--- Redirects
-setAllowRedirects :: Bool -> BrowserAction ()
-setAllowRedirects bl = alterBS (\b -> b {bsAllowRedirects=bl})
-
-getAllowRedirects :: BrowserAction Bool
-getAllowRedirects = getBS bsAllowRedirects
-
-
--- Proxy
-setProxy :: Proxy -> BrowserAction ()
-setProxy p = alterBS (\b -> b {bsProxy = p})
-
-getProxy :: BrowserAction Proxy
-getProxy = getBS bsProxy
-
-
--- Debug
-setDebugLog :: Maybe String -> BrowserAction ()
-setDebugLog v = alterBS (\b -> b {bsDebug=v})
-
-
--- Page control
-type RequestState = ( Int    -- number of 401 responses so far
-                    , Int    -- number of redirects so far
-                    , Int    -- number of retrys so far
-                    , Bool   -- whether to pre-empt 401 response
-                    )
-
-
-
--- Surely the most important bit:
-request :: Request -> BrowserAction (URI,Response)
-request = request' initialState
-    where
-        initialState = (0,0,0,True)
-
-
-request' :: RequestState -> Request -> BrowserAction (URI,Response)
-request' (denycount,redirectcount,retrycount,preempt) rq =
-    do -- add cookies to request
-       let uri = rqURI rq
-       cookies <- getCookiesFor (uriToAuthorityString uri) (uriPath uri)
-       
-       when (not $ null cookies) 
-            (out $ "Adding cookies to request.  Cookie names: " 
-                 ++ foldl spaceappend "" (map ckName cookies))
-       
-       -- add credentials to request
-       rq' <- if not preempt then return rq else
-              do { auth <- anticipateChallenge rq
-                 ; case auth of
-                     Just x  -> return (insertHeader HdrAuthorization (withAuthority x rq) rq)
-                     Nothing -> return rq
-                 }
-               
-       let rq'' = insertHeaders (map cookieToHeader cookies) rq'
-
-       p <- getProxy
-
-       out ("Sending:\n" ++ show rq'') 
-       e_rsp <- case p of
-            NoProxy -> dorequest (uriAuth $ rqURI rq'') rq''
-            Proxy str ath ->
-                let rq''' = case ath of 
-                                Nothing -> rq''
-                                Just x  -> insertHeader HdrProxyAuthorization (withAuthority x rq'') rq''
-                in dorequest (URIAuth "" str "") rq'''
-
-       case e_rsp of
-           Left v -> if (retrycount < 4) && (v == ErrorReset || v == ErrorClosed)
-               then request' (denycount,redirectcount,retrycount+1,preempt) rq
-               else error ("Exception raised in request: " ++ show v)
-           Right rsp -> do 
-               out ("Received:\n" ++ show rsp)
-
-               -- add new cookies to browser state
-               let cookieheaders = retrieveHeaders HdrSetCookie rsp
-               let newcookies = concat (map (headerToCookies $ uriToAuthorityString uri) cookieheaders)
-
-               when (not $ null newcookies)
-                    (out $ foldl (\x y -> x ++ "\n  " ++ show y) "Cookies received:" newcookies)
-               
-               filterfn <- getCookieFilter
-               newcookies' <- ioAction (filterM (filterfn uri) newcookies)
-               foldM (\_ -> addCookie) () newcookies'
-
-               when (not $ null newcookies)
-                    (out $ "Accepting cookies with names: " ++ foldl spaceappend "" (map ckName newcookies'))
-       
-               case rspCode rsp of
-                   (4,0,1) ->  -- Credentials not sent or refused.
-                       out "401 - credentials not sent or refused" >>
-                       if denycount > 2 then return (uri,rsp) else
-                       do { let hdrs = retrieveHeaders HdrWWWAuthenticate rsp
-                          ; case pickChallenge (catMaybes $ map (headerToChallenge uri) hdrs) of
-                                Just x  ->
-                                    do { au <- challengeToAuthority uri x
-                                       ; case au of
-                                            Just au' ->
-                                                out "Retrying request with new credentials" >>
-                                                request' (denycount+1,redirectcount,retrycount,False)
-                                                         (insertHeader HdrAuthorization (withAuthority au' rq) rq)
-                                            Nothing  -> return (uri,rsp)   {- do nothing -}
-                                       }
-                                          
-                                Nothing -> return (uri,rsp)   {- do nothing -}
-                          }
-                   
-
-                   (4,0,7) ->  -- Proxy Authentication required
-                       out "407 - proxy authentication required" >>
-                       if denycount > 2 then return (uri,rsp) else
-                       do { let hdrs = retrieveHeaders HdrProxyAuthenticate rsp
-                          ; case pickChallenge (catMaybes $ map (headerToChallenge uri) hdrs) of
-                                Just x  ->
-                                    do { au <- challengeToAuthority uri x
-                                       ; case au of
-                                            Just au' ->
-                                                do { pxy <- getBS bsProxy
-                                                   ; case pxy of
-                                                        NoProxy ->
-                                                            do { err "Proxy authentication required without proxy!"
-                                                               ; return (uri,rsp)
-                                                               }
-                                                        Proxy x y ->
-                                                            do { out "Retrying with proxy authentication"
-                                                               ; setProxy (Proxy x $ Just au')
-                                                               ; request' (denycount+1,redirectcount,retrycount,False) rq
-                                                               }
-                                                   }                                                      
-                                            Nothing  -> return (uri,rsp)   {- do nothing -}
-                                       }
-                                          
-                                Nothing -> return (uri,rsp)   {- do nothing -}
-                          }
-
-
-                   (3,0,3) ->  -- Redirect using GET request method.
-                       do { out "303 - redirect using GET"
-                          ; rd <- getAllowRedirects
-                          ; if not rd || redirectcount > 4 then return (uri,rsp) else
-                            case retrieveHeaders HdrLocation rsp of
-                                (Header _ u:_) -> case parseURIReference u of
-                                    Just newuri ->
-                                        let newuri' = case newuri `relativeTo` uri of
-                                                        Nothing -> newuri
-                                                        Just x  -> x
-                                        in do { out ("Redirecting to " ++ show newuri' ++ " ...") 
-                                              ; let rq = rq { rqMethod=GET, rqURI=newuri', rqBody="" }
-                                              ; request' (0,redirectcount+1,retrycount,True)
-                                                         (replaceHeader HdrContentLength "0" rq) 
-                                              }
-                                    Nothing ->
-                                        do { err ("Parse of Location header in a redirect response failed: " ++ u)
-                                           ; return (uri,rsp)
-                                           }
-                                [] -> do { err "No Location header in redirect response"
-                                         ; return (uri,rsp)
-                                         }
-                          }
-                        
-                   (3,0,5) ->
-                        case retrieveHeaders HdrLocation rsp of
-                            (Header _ u:_) -> case parseURIReference u of
-                                Just newuri ->
-                                    do { out ("Retrying with proxy " ++ show newuri ++ "...")
-                                       ; setProxy (Proxy (uriToAuthorityString newuri) Nothing)
-                                       ; request' (0,0,retrycount+1,True) rq
-                                       }
-                                Nothing ->
-                                    do { err ("Parse of Location header in a proxy redirect response failed: " ++ u)
-                                       ; return (uri,rsp)
-                                       }
-                            [] -> do { err "No Location header in proxy redirect response."
-                                     ; return (uri,rsp)
-                                     }
-                   
-
-                   (3,_,_) ->  redirect uri rsp
-                   _       -> return (uri,rsp)
-
-    where      
-        spaceappend :: String -> String -> String
-        spaceappend x y = x ++ ' ' : y
-
-        redirect uri rsp = do
-            rd <- getAllowRedirects
-            if not rd || redirectcount > 4 then return (uri,rsp) else do
-            case retrieveHeaders HdrLocation rsp of
-              (Header _ u:_) -> case parseURIReference u of
-                                  Just newuri -> do
-                                      let newuri' = case newuri `relativeTo` uri of
-                                                      Nothing -> newuri
-                                                      Just x  -> x
-                                      out ("Redirecting to " ++ show newuri' ++ " ...") 
-                                      request' (0,redirectcount+1,retrycount,True) (rq { rqURI=newuri' }) 
-                                  Nothing -> do
-                                      err ("Parse of Location header in a redirect response failed: " ++ u)
-                                      return (uri,rsp)
-              [] -> do err "No Location header in redirect response."
-                       return (uri,rsp)
-
-
-        dorequest :: URIAuth -> Request -> BrowserAction (Either ConnError Response)
-        dorequest hst rqst = 
-            do { pool <- getBS bsConnectionPool
-               ; conn <- ioAction $ filterM (\c -> c `isConnectedTo` uriAuthToString hst) pool
-               ; rsp <- case conn of
-                    [] -> do { out ("Creating new connection to " ++ uriAuthToString hst)
-                             ; let port = case uriPort hst of
-                                            (':':s) -> read s
-                                            _       -> 80
-                             ; c <- ioAction $ openTCPPort (uriRegName hst) port
-                             ; let pool' = if length pool > 5
-                                           then init pool
-                                           else pool
-                             ; when (length pool > 5)
-                                    (ioAction $ close (last pool))
-                             ; alterBS (\b -> b { bsConnectionPool=c:pool' })
-                             ; dorequest2 hst c rqst
-                             }
-                    (c:_) ->
-                        do { out ("Recovering connection to " ++ uriAuthToString hst)
-                           ; dorequest2 hst c rqst
-                           }
-               ; 
-               ; return rsp
-               }
-
-        dorequest2 hst c r =
-            do { dbg <- getBS bsDebug
-               ; ioAction $ case dbg of
-                 Nothing -> sendHTTP c r
-                 Just f  ->
-                    debugStream (f++'-': uriAuthToString hst) c 
-                    >>= \c' -> sendHTTP c' r  
-               }
- 
-
-
-uriAuth x = case uriAuthority x of
-              Just ua -> ua
-              _       -> error ("No uri authority for: "++show x)
-
-uriAuthToString :: URIAuth -> String
-uriAuthToString URIAuth { uriUserInfo = uinfo
-                        , uriRegName  = regname
-                        , uriPort     = port
-                        } =
-                    ((if null uinfo then id else (uinfo++))
-                     . (regname++)
-                     . (port++)) ""
-
--- This function duplicates old Network.URI.authority behaviour.
-uriToAuthorityString :: URI -> String
-uriToAuthorityString u = maybe "" uriAuthToString (uriAuthority u)
-
-
-
-------------------------------------------------------------------
------------------- Request Building ------------------------------
-------------------------------------------------------------------
-
-
-libUA = "haskell-libwww/0.1"
-
-defaultGETRequest :: URI -> Request
-defaultGETRequest uri = 
-    Request { rqURI=uri
-            , rqBody=""
-            , rqHeaders=[ Header HdrContentLength "0"
-                        , Header HdrUserAgent libUA
-                        ]
-            , rqMethod=GET
-            }
-
-
-
--- This form junk is completely untested...
-
-type FormVar = (String,String)
-
-data Form = Form RequestMethod URI [FormVar]
-
-
-formToRequest :: Form -> Request
-formToRequest (Form m u vs) =
-    let enc = urlEncodeVars vs
-    in case m of
-        GET -> Request { rqMethod=GET
-                       , rqHeaders=[ Header HdrContentLength "0" ]
-                       , rqBody=""
-                       , rqURI=u { uriQuery= '?' : enc }  -- What about old query?
-                       }
-        POST -> Request { rqMethod=POST
-                        , rqHeaders=[ Header HdrContentType "application/x-www-form-urlencoded",
-                                      Header HdrContentLength (show $ length enc) ]
-                        , rqBody=enc
-                        , rqURI=u
-                        }
+{-# LANGUAGE MultiParamTypeClasses, GeneralizedNewtypeDeriving, CPP, FlexibleContexts #-}
+{- |
+
+Module      :  Network.Browser
+Copyright   :  See LICENSE file
+License     :  BSD
+
+Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+Stability   :  experimental
+Portability :  non-portable (not tested)
+
+Session-level interactions over HTTP.
+
+The "Network.Browser" goes beyond the basic "Network.HTTP" functionality in
+providing support for more involved, and real, request/response interactions over
+HTTP. Additional features supported are:
+
+* HTTP Authentication handling
+
+* Transparent handling of redirects
+
+* Cookie stores + transmission.
+
+* Transaction logging
+
+* Proxy-mediated connections.
+
+Example use:
+
+>    do
+>      (_, rsp)
+>         <- Network.Browser.browse $ do
+>               setAllowRedirects True -- handle HTTP redirects
+>               request $ getRequest "http://www.haskell.org/"
+>      return (take 100 (rspBody rsp))
+
+-}
+module Network.Browser
+       ( BrowserState
+       , BrowserAction      -- browser monad, effectively a state monad.
+       , Proxy(..)
+
+       , browse             -- :: BrowserAction a -> IO a
+       , request            -- :: Request -> BrowserAction Response
+
+       , getBrowserState    -- :: BrowserAction t (BrowserState t)
+       , withBrowserState   -- :: BrowserState t -> BrowserAction t a -> BrowserAction t a
+
+       , setAllowRedirects  -- :: Bool -> BrowserAction t ()
+       , getAllowRedirects  -- :: BrowserAction t Bool
+
+       , setMaxRedirects    -- :: Int -> BrowserAction t ()
+       , getMaxRedirects    -- :: BrowserAction t (Maybe Int)
+
+       , Authority(..)
+       , getAuthorities
+       , setAuthorities
+       , addAuthority
+       , Challenge(..)
+       , Qop(..)
+       , Algorithm(..)
+
+       , getAuthorityGen
+       , setAuthorityGen
+       , setAllowBasicAuth
+       , getAllowBasicAuth
+
+       , setMaxErrorRetries  -- :: Maybe Int -> BrowserAction t ()
+       , getMaxErrorRetries  -- :: BrowserAction t (Maybe Int)
+
+       , setMaxPoolSize     -- :: Int -> BrowserAction t ()
+       , getMaxPoolSize     -- :: BrowserAction t (Maybe Int)
+
+       , setMaxAuthAttempts  -- :: Maybe Int -> BrowserAction t ()
+       , getMaxAuthAttempts  -- :: BrowserAction t (Maybe Int)
+
+       , setCookieFilter     -- :: (URI -> Cookie -> IO Bool) -> BrowserAction t ()
+       , getCookieFilter     -- :: BrowserAction t (URI -> Cookie -> IO Bool)
+       , defaultCookieFilter -- :: URI -> Cookie -> IO Bool
+       , userCookieFilter    -- :: URI -> Cookie -> IO Bool
+
+       , Cookie(..)
+       , getCookies        -- :: BrowserAction t [Cookie]
+       , setCookies        -- :: [Cookie] -> BrowserAction t ()
+       , addCookie         -- :: Cookie   -> BrowserAction t ()
+
+       , setErrHandler     -- :: (String -> IO ()) -> BrowserAction t ()
+       , setOutHandler     -- :: (String -> IO ()) -> BrowserAction t ()
+
+       , setEventHandler   -- :: (BrowserEvent -> BrowserAction t ()) -> BrowserAction t ()
+
+       , BrowserEvent(..)
+       , BrowserEventType(..)
+       , RequestID
+
+       , setProxy         -- :: Proxy -> BrowserAction t ()
+       , getProxy         -- :: BrowserAction t Proxy
+
+       , setCheckForProxy -- :: Bool -> BrowserAction t ()
+       , getCheckForProxy -- :: BrowserAction t Bool
+
+       , setDebugLog      -- :: Maybe String -> BrowserAction t ()
+
+       , getUserAgent     -- :: BrowserAction t String
+       , setUserAgent     -- :: String -> BrowserAction t ()
+
+       , out              -- :: String -> BrowserAction t ()
+       , err              -- :: String -> BrowserAction t ()
+       , ioAction         -- :: IO a -> BrowserAction a
+
+       , defaultGETRequest
+       , defaultGETRequest_
+
+       , formToRequest
+       , uriDefaultTo
+
+         -- old and half-baked; don't use:
+       , Form(..)
+       , FormVar
+       ) where
+
+import Network.URI
+   ( URI(..)
+   , URIAuth(..)
+   , parseURI, parseURIReference, relativeTo
+   )
+import Network.StreamDebugger (debugByteStream)
+import Network.HTTP hiding ( sendHTTP_notify )
+import Network.HTTP.HandleStream ( sendHTTP_notify )
+import Network.HTTP.Auth
+import Network.HTTP.Cookie
+import Network.HTTP.Proxy
+
+import Network.Stream ( ConnError(..), Result )
+import Network.BufferType
+#if (MIN_VERSION_base(4,9,0)) && !(MIN_VERSION_base(4,13,0))
+import Control.Monad.Fail
+#endif
+
+import Data.Char (toLower)
+import Data.List (isPrefixOf)
+import Data.Maybe (fromMaybe, listToMaybe, catMaybes )
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative (Applicative (..), (<$>))
+#endif
+import Control.Monad (filterM, forM_, when)
+import Control.Monad.IO.Class
+   ( MonadIO (..) )
+import Control.Monad.State
+   ( MonadState(..), gets, modify, StateT (..), evalStateT, withStateT )
+
+import qualified System.IO
+   ( hSetBuffering, hPutStr, stdout, stdin, hGetChar
+   , BufferMode(NoBuffering, LineBuffering)
+   )
+import Data.Time.Clock ( UTCTime, getCurrentTime )
+
+
+------------------------------------------------------------------
+----------------------- Cookie Stuff -----------------------------
+------------------------------------------------------------------
+
+-- | @defaultCookieFilter@ is the initial cookie acceptance filter.
+-- It welcomes them all into the store @:-)@
+defaultCookieFilter :: URI -> Cookie -> IO Bool
+defaultCookieFilter _url _cky = return True
+
+-- | @userCookieFilter@ is a handy acceptance filter, asking the
+-- user if he/she is willing to accept an incoming cookie before
+-- adding it to the store.
+userCookieFilter :: URI -> Cookie -> IO Bool
+userCookieFilter url cky = do
+    do putStrLn ("Set-Cookie received when requesting: " ++ show url)
+       case ckComment cky of
+          Nothing -> return ()
+          Just x  -> putStrLn ("Cookie Comment:\n" ++ x)
+       let pth = maybe "" ('/':) (ckPath cky)
+       putStrLn ("Domain/Path: " ++ ckDomain cky ++ pth)
+       putStrLn (ckName cky ++ '=' : ckValue cky)
+       System.IO.hSetBuffering System.IO.stdout System.IO.NoBuffering
+       System.IO.hSetBuffering System.IO.stdin System.IO.NoBuffering
+       System.IO.hPutStr System.IO.stdout "Accept [y/n]? "
+       x <- System.IO.hGetChar System.IO.stdin
+       System.IO.hSetBuffering System.IO.stdin System.IO.LineBuffering
+       System.IO.hSetBuffering System.IO.stdout System.IO.LineBuffering
+       return (toLower x == 'y')
+
+-- | @addCookie c@ adds a cookie to the browser state, removing duplicates.
+addCookie :: Cookie -> BrowserAction t ()
+addCookie c = modify (\b -> b{bsCookies = c : filter (/=c) (bsCookies b) })
+
+-- | @setCookies cookies@ replaces the set of cookies known to
+-- the browser to @cookies@. Useful when wanting to restore cookies
+-- used across 'browse' invocations.
+setCookies :: [Cookie] -> BrowserAction t ()
+setCookies cs = modify (\b -> b { bsCookies=cs })
+
+-- | @getCookies@ returns the current set of cookies known to
+-- the browser.
+getCookies :: BrowserAction t [Cookie]
+getCookies = gets bsCookies
+
+-- ...get domain specific cookies...
+-- ... this needs changing for consistency with rfc2109...
+-- ... currently too broad.
+getCookiesFor :: String -> String -> BrowserAction t [Cookie]
+getCookiesFor dom path =
+    do cks <- getCookies
+       return (filter cookiematch cks)
+    where
+        cookiematch :: Cookie -> Bool
+        cookiematch = cookieMatch (dom,path)
+
+
+-- | @setCookieFilter fn@ sets the cookie acceptance filter to @fn@.
+setCookieFilter :: (URI -> Cookie -> IO Bool) -> BrowserAction t ()
+setCookieFilter f = modify (\b -> b { bsCookieFilter=f })
+
+-- | @getCookieFilter@ returns the current cookie acceptance filter.
+getCookieFilter :: BrowserAction t (URI -> Cookie -> IO Bool)
+getCookieFilter = gets bsCookieFilter
+
+------------------------------------------------------------------
+----------------------- Authorisation Stuff ----------------------
+------------------------------------------------------------------
+
+{-
+
+The browser handles 401 responses in the following manner:
+  1) extract all WWW-Authenticate headers from a 401 response
+  2) rewrite each as a Challenge object, using "headerToChallenge"
+  3) pick a challenge to respond to, usually the strongest
+     challenge understood by the client, using "pickChallenge"
+  4) generate a username/password combination using the browsers
+     "bsAuthorityGen" function (the default behaviour is to ask
+     the user)
+  5) build an Authority object based upon the challenge and user
+     data, store this new Authority in the browser state
+  6) convert the Authority to a request header and add this
+     to a request using "withAuthority"
+  7) send the amended request
+
+Note that by default requests are annotated with authority headers
+before the first sending, based upon previously generated Authority
+objects (which contain domain information).  Once a specific authority
+is added to a rejected request this predictive annotation is suppressed.
+
+407 responses are handled in a similar manner, except
+   a) Authorities are not collected, only a single proxy authority
+      is kept by the browser
+   b) If the proxy used by the browser (type Proxy) is NoProxy, then
+      a 407 response will generate output on the "err" stream and
+      the response will be returned.
+
+
+Notes:
+ - digest authentication so far ignores qop, so fails to authenticate
+   properly with qop=auth-int challenges
+ - calculates a1 more than necessary
+ - doesn't reverse authenticate
+ - doesn't properly receive AuthenticationInfo headers, so fails
+   to use next-nonce etc
+
+-}
+
+-- | Return authorities for a given domain and path.
+-- Assumes "dom" is lower case
+getAuthFor :: String -> String -> BrowserAction t [Authority]
+getAuthFor dom pth = getAuthorities >>= return . (filter match)
+   where
+    match :: Authority -> Bool
+    match au@AuthBasic{}  = matchURI (auSite au)
+    match au@AuthDigest{} = or (map matchURI (auDomain au))
+
+    matchURI :: URI -> Bool
+    matchURI s = (uriToAuthorityString s == dom) && (uriPath s `isPrefixOf` pth)
+
+
+-- | @getAuthorities@ return the current set of @Authority@s known
+-- to the browser.
+getAuthorities :: BrowserAction t [Authority]
+getAuthorities = gets bsAuthorities
+
+-- @setAuthorities as@ replaces the Browser's known set
+-- of 'Authority's to @as@.
+setAuthorities :: [Authority] -> BrowserAction t ()
+setAuthorities as = modify (\b -> b { bsAuthorities=as })
+
+-- @addAuthority a@ adds 'Authority' @a@ to the Browser's
+-- set of known authorities.
+addAuthority :: Authority -> BrowserAction t ()
+addAuthority a = modify (\b -> b { bsAuthorities=a:bsAuthorities b })
+
+-- | @getAuthorityGen@ returns the current authority generator
+getAuthorityGen :: BrowserAction t (URI -> String -> IO (Maybe (String,String)))
+getAuthorityGen = gets bsAuthorityGen
+
+-- | @setAuthorityGen genAct@ sets the auth generator to @genAct@.
+setAuthorityGen :: (URI -> String -> IO (Maybe (String,String))) -> BrowserAction t ()
+setAuthorityGen f = modify (\b -> b { bsAuthorityGen=f })
+
+-- | @setAllowBasicAuth onOff@ enables\/disables HTTP Basic Authentication.
+setAllowBasicAuth :: Bool -> BrowserAction t ()
+setAllowBasicAuth ba = modify (\b -> b { bsAllowBasicAuth=ba })
+
+getAllowBasicAuth :: BrowserAction t Bool
+getAllowBasicAuth = gets bsAllowBasicAuth
+
+-- | @setMaxAuthAttempts mbMax@ sets the maximum number of authentication attempts
+-- to do. If @Nothing@, revert to default max.
+setMaxAuthAttempts :: Maybe Int -> BrowserAction t ()
+setMaxAuthAttempts mb
+ | fromMaybe 0 mb < 0 = return ()
+ | otherwise          = modify (\ b -> b{bsMaxAuthAttempts=mb})
+
+-- | @getMaxAuthAttempts@ returns the current max auth attempts. If @Nothing@,
+-- the browser's default is used.
+getMaxAuthAttempts :: BrowserAction t (Maybe Int)
+getMaxAuthAttempts = gets bsMaxAuthAttempts
+
+-- | @setMaxErrorRetries mbMax@ sets the maximum number of attempts at
+-- transmitting a request. If @Nothing@, rever to default max.
+setMaxErrorRetries :: Maybe Int -> BrowserAction t ()
+setMaxErrorRetries mb
+ | fromMaybe 0 mb < 0 = return ()
+ | otherwise          = modify (\ b -> b{bsMaxErrorRetries=mb})
+
+-- | @getMaxErrorRetries@ returns the current max number of error retries.
+getMaxErrorRetries :: BrowserAction t (Maybe Int)
+getMaxErrorRetries = gets bsMaxErrorRetries
+
+-- TO BE CHANGED!!!
+pickChallenge :: Bool -> [Challenge] -> Maybe Challenge
+pickChallenge allowBasic []
+ | allowBasic = Just (ChalBasic "/") -- manufacture a challenge if one missing; more robust.
+pickChallenge _ ls = listToMaybe ls
+
+-- | Retrieve a likely looking authority for a Request.
+anticipateChallenge :: Request ty -> BrowserAction t (Maybe Authority)
+anticipateChallenge rq =
+    let uri = rqURI rq in
+    do { authlist <- getAuthFor (uriAuthToString $ reqURIAuth rq) (uriPath uri)
+       ; return (listToMaybe authlist)
+       }
+
+-- | Asking the user to respond to a challenge
+challengeToAuthority :: URI -> Challenge -> BrowserAction t (Maybe Authority)
+challengeToAuthority uri ch
+ | not (answerable ch) = return Nothing
+ | otherwise = do
+      -- prompt user for authority
+    prompt <- getAuthorityGen
+    userdetails <- liftIO $ prompt uri (chRealm ch)
+    case userdetails of
+     Nothing    -> return Nothing
+     Just (u,p) -> return (Just $ buildAuth ch u p)
+ where
+  answerable :: Challenge -> Bool
+  answerable ChalBasic{} = True
+  answerable chall       = (chAlgorithm chall) == Just AlgMD5
+
+  buildAuth :: Challenge -> String -> String -> Authority
+  buildAuth (ChalBasic r) u p =
+       AuthBasic { auSite=uri
+                 , auRealm=r
+                 , auUsername=u
+                 , auPassword=p
+                 }
+
+    -- note to self: this is a pretty stupid operation
+    -- to perform isn't it? ChalX and AuthX are so very
+    -- similar.
+  buildAuth (ChalDigest r d n o _stale a q) u p =
+            AuthDigest { auRealm=r
+                       , auUsername=u
+                       , auPassword=p
+                       , auDomain=d
+                       , auNonce=n
+                       , auOpaque=o
+                       , auAlgorithm=a
+                       , auQop=q
+                       }
+
+
+------------------------------------------------------------------
+------------------ Browser State Actions -------------------------
+------------------------------------------------------------------
+
+
+-- | @BrowserState@ is the (large) record type tracking the current
+-- settings of the browser.
+data BrowserState connection
+ = BS { bsErr, bsOut      :: String -> IO ()
+      , bsCookies         :: [Cookie]
+      , bsCookieFilter    :: URI -> Cookie -> IO Bool
+      , bsAuthorityGen    :: URI -> String -> IO (Maybe (String,String))
+      , bsAuthorities     :: [Authority]
+      , bsAllowRedirects  :: Bool
+      , bsAllowBasicAuth  :: Bool
+      , bsMaxRedirects    :: Maybe Int
+      , bsMaxErrorRetries :: Maybe Int
+      , bsMaxAuthAttempts :: Maybe Int
+      , bsMaxPoolSize     :: Maybe Int
+      , bsConnectionPool  :: [connection]
+      , bsCheckProxy      :: Bool
+      , bsProxy           :: Proxy
+      , bsDebug           :: Maybe String
+      , bsEvent           :: Maybe (BrowserEvent -> BrowserAction connection ())
+      , bsRequestID       :: RequestID
+      , bsUserAgent       :: Maybe String
+      }
+
+instance Show (BrowserState t) where
+    show bs =  "BrowserState { "
+            ++ shows (bsCookies bs) ("\n"
+           {- ++ show (bsAuthorities bs) ++ "\n"-}
+            ++ "AllowRedirects: " ++ shows (bsAllowRedirects bs) "} ")
+
+-- | @BrowserAction@ is the IO monad, but carrying along a 'BrowserState'.
+newtype BrowserAction conn a
+ = BA { unBA :: StateT (BrowserState conn) IO a }
+ deriving
+ ( Functor, Applicative, Monad, MonadIO, MonadState (BrowserState conn)
+#if MIN_VERSION_base(4,9,0)
+ , MonadFail
+#endif
+ )
+
+runBA :: BrowserState conn -> BrowserAction conn a -> IO a
+runBA bs = flip evalStateT bs . unBA
+
+-- | @browse act@ is the toplevel action to perform a 'BrowserAction'.
+-- Example use: @browse (request (getRequest yourURL))@.
+browse :: BrowserAction conn a -> IO a
+browse = runBA defaultBrowserState
+
+-- | The default browser state has the settings
+defaultBrowserState :: BrowserState t
+defaultBrowserState = res
+ where
+   res = BS
+     { bsErr              = putStrLn
+     , bsOut              = putStrLn
+     , bsCookies          = []
+     , bsCookieFilter     = defaultCookieFilter
+     , bsAuthorityGen     = \ _uri _realm -> do
+          bsErr res "No action for prompting/generating user+password credentials provided (use: setAuthorityGen); returning Nothing"
+          return Nothing
+     , bsAuthorities      = []
+     , bsAllowRedirects   = True
+     , bsAllowBasicAuth   = False
+     , bsMaxRedirects     = Nothing
+     , bsMaxErrorRetries  = Nothing
+     , bsMaxAuthAttempts  = Nothing
+     , bsMaxPoolSize      = Nothing
+     , bsConnectionPool   = []
+     , bsCheckProxy       = defaultAutoProxyDetect
+     , bsProxy            = noProxy
+     , bsDebug            = Nothing
+     , bsEvent            = Nothing
+     , bsRequestID        = 0
+     , bsUserAgent        = Nothing
+     }
+
+{-# DEPRECATED getBrowserState "Use Control.Monad.State.get instead." #-}
+-- | @getBrowserState@ returns the current browser config. Useful
+-- for restoring state across 'BrowserAction's.
+getBrowserState :: BrowserAction t (BrowserState t)
+getBrowserState = get
+
+-- | @withBrowserAction st act@ performs @act@ with 'BrowserState' @st@.
+withBrowserState :: BrowserState t -> BrowserAction t a -> BrowserAction t a
+withBrowserState bs = BA . withStateT (const bs) . unBA
+
+-- | @nextRequest act@ performs the browser action @act@ as
+-- the next request, i.e., setting up a new request context
+-- before doing so.
+nextRequest :: BrowserAction t a -> BrowserAction t a
+nextRequest act = do
+  let updReqID st =
+       let
+        rid = succ (bsRequestID st)
+       in
+       rid `seq` st{bsRequestID=rid}
+  modify updReqID
+  act
+
+-- | Lifts an IO action into the 'BrowserAction' monad.
+{-# DEPRECATED ioAction "Use Control.Monad.Trans.liftIO instead." #-}
+ioAction :: IO a -> BrowserAction t a
+ioAction = liftIO
+
+-- | @setErrHandler@ sets the IO action to call when
+-- the browser reports running errors. To disable any
+-- such, set it to @const (return ())@.
+setErrHandler :: (String -> IO ()) -> BrowserAction t ()
+setErrHandler h = modify (\b -> b { bsErr=h })
+
+-- | @setOutHandler@ sets the IO action to call when
+-- the browser chatters info on its running. To disable any
+-- such, set it to @const (return ())@.
+setOutHandler :: (String -> IO ()) -> BrowserAction t ()
+setOutHandler h = modify (\b -> b { bsOut=h })
+
+out, err :: String -> BrowserAction t ()
+out s = do { f <- gets bsOut ; liftIO $ f s }
+err s = do { f <- gets bsErr ; liftIO $ f s }
+
+-- | @setAllowRedirects onOff@ toggles the willingness to
+-- follow redirects (HTTP responses with 3xx status codes).
+setAllowRedirects :: Bool -> BrowserAction t ()
+setAllowRedirects bl = modify (\b -> b {bsAllowRedirects=bl})
+
+-- | @getAllowRedirects@ returns current setting of the do-chase-redirects flag.
+getAllowRedirects :: BrowserAction t Bool
+getAllowRedirects = gets bsAllowRedirects
+
+-- | @setMaxRedirects maxCount@ sets the maximum number of forwarding hops
+-- we are willing to jump through. A no-op if the count is negative; if zero,
+-- the max is set to whatever default applies. Notice that setting the max
+-- redirects count does /not/ enable following of redirects itself; use
+-- 'setAllowRedirects' to do so.
+setMaxRedirects :: Maybe Int -> BrowserAction t ()
+setMaxRedirects c
+ | fromMaybe 0 c < 0  = return ()
+ | otherwise          = modify (\b -> b{bsMaxRedirects=c})
+
+-- | @getMaxRedirects@ returns the current setting for the max-redirect count.
+-- If @Nothing@, the "Network.Browser"'s default is used.
+getMaxRedirects :: BrowserAction t (Maybe Int)
+getMaxRedirects = gets bsMaxRedirects
+
+-- | @setMaxPoolSize maxCount@ sets the maximum size of the connection pool
+-- that is used to cache connections between requests
+setMaxPoolSize :: Maybe Int -> BrowserAction t ()
+setMaxPoolSize c = modify (\b -> b{bsMaxPoolSize=c})
+
+-- | @getMaxPoolSize@ gets the maximum size of the connection pool
+-- that is used to cache connections between requests.
+-- If @Nothing@, the "Network.Browser"'s default is used.
+getMaxPoolSize :: BrowserAction t (Maybe Int)
+getMaxPoolSize = gets bsMaxPoolSize
+
+-- | @setProxy p@ will disable proxy usage if @p@ is @NoProxy@.
+-- If @p@ is @Proxy proxyURL mbAuth@, then @proxyURL@ is interpreted
+-- as the URL of the proxy to use, possibly authenticating via
+-- 'Authority' information in @mbAuth@.
+setProxy :: Proxy -> BrowserAction t ()
+setProxy p =
+   -- Note: if user _explicitly_ sets the proxy, we turn
+   -- off any auto-detection of proxies.
+  modify (\b -> b {bsProxy = p, bsCheckProxy=False})
+
+-- | @getProxy@ returns the current proxy settings. If
+-- the auto-proxy flag is set to @True@, @getProxy@ will
+-- perform the necessary
+getProxy :: BrowserAction t Proxy
+getProxy = do
+  p <- gets bsProxy
+  case p of
+      -- Note: if there is a proxy, no need to perform any auto-detect.
+      -- Presumably this is the user's explicit and preferred proxy server.
+    Proxy{} -> return p
+    NoProxy{} -> do
+     flg <- gets bsCheckProxy
+     if not flg
+      then return p
+      else do
+       np <- liftIO $ fetchProxy True{-issue warning on stderr if ill-formed...-}
+        -- note: this resets the check-proxy flag; a one-off affair.
+       setProxy np
+       return np
+
+-- | @setCheckForProxy flg@ sets the one-time check for proxy
+-- flag to @flg@. If @True@, the session will try to determine
+-- the proxy server is locally configured. See 'Network.HTTP.Proxy.fetchProxy'
+-- for details of how this done.
+setCheckForProxy :: Bool -> BrowserAction t ()
+setCheckForProxy flg = modify (\ b -> b{bsCheckProxy=flg})
+
+-- | @getCheckForProxy@ returns the current check-proxy setting.
+-- Notice that this may not be equal to @True@ if the session has
+-- set it to that via 'setCheckForProxy' and subsequently performed
+-- some HTTP protocol interactions. i.e., the flag return represents
+-- whether a proxy will be checked for again before any future protocol
+-- interactions.
+getCheckForProxy :: BrowserAction t Bool
+getCheckForProxy = gets bsCheckProxy
+
+-- | @setDebugLog mbFile@ turns off debug logging iff @mbFile@
+-- is @Nothing@. If set to @Just fStem@, logs of browser activity
+-- is appended to files of the form @fStem-url-authority@, i.e.,
+-- @fStem@ is just the prefix for a set of log files, one per host/authority.
+setDebugLog :: Maybe String -> BrowserAction t ()
+setDebugLog v = modify (\b -> b {bsDebug=v})
+
+-- | @setUserAgent ua@ sets the current @User-Agent:@ string to @ua@. It
+-- will be used if no explicit user agent header is found in subsequent requests.
+--
+-- A common form of user agent string is @\"name\/version (details)\"@. For
+-- example @\"cabal-install/0.10.2 (HTTP 4000.1.2)\"@. Including the version
+-- of this HTTP package can be helpful if you ever need to track down HTTP
+-- compatibility quirks. This version is available via 'httpPackageVersion'.
+-- For more info see <http://en.wikipedia.org/wiki/User_agent>.
+--
+setUserAgent :: String -> BrowserAction t ()
+setUserAgent ua = modify (\b -> b{bsUserAgent=Just ua})
+
+-- | @getUserAgent@ returns the current @User-Agent:@ default string.
+getUserAgent :: BrowserAction t String
+getUserAgent  = do
+  n <- gets bsUserAgent
+  return (maybe defaultUserAgent id n)
+
+-- | @RequestState@ is an internal tallying type keeping track of various
+-- per-connection counters, like the number of authorization attempts and
+-- forwards we've gone through.
+data RequestState
+  = RequestState
+      { reqDenies     :: Int   -- ^ number of 401 responses so far
+      , reqRedirects  :: Int   -- ^ number of redirects so far
+      , reqRetries    :: Int   -- ^ number of retries so far
+      , reqStopOnDeny :: Bool  -- ^ whether to pre-empt 401 response
+      }
+
+type RequestID = Int -- yeah, it will wrap around.
+
+nullRequestState :: RequestState
+nullRequestState = RequestState
+      { reqDenies     = 0
+      , reqRedirects  = 0
+      , reqRetries    = 0
+      , reqStopOnDeny = True
+      }
+
+-- | @BrowserEvent@ is the event record type that a user-defined handler, set
+-- via 'setEventHandler', will be passed. It indicates various state changes
+-- encountered in the processing of a given 'RequestID', along with timestamps
+-- at which they occurred.
+data BrowserEvent
+ = BrowserEvent
+      { browserTimestamp  :: UTCTime
+      , browserRequestID  :: RequestID
+      , browserRequestURI :: {-URI-}String
+      , browserEventType  :: BrowserEventType
+      }
+
+-- | 'BrowserEventType' is the enumerated list of events that the browser
+-- internals will report to a user-defined event handler.
+data BrowserEventType
+ = OpenConnection
+ | ReuseConnection
+ | RequestSent
+ | ResponseEnd ResponseData
+ | ResponseFinish
+{- not yet, you will have to determine these via the ResponseEnd event.
+ | Redirect
+ | AuthChallenge
+ | AuthResponse
+-}
+
+-- | @setEventHandler onBrowserEvent@ configures event handling.
+-- If @onBrowserEvent@ is @Nothing@, event handling is turned off;
+-- setting it to @Just onEv@ causes the @onEv@ IO action to be
+-- notified of browser events during the processing of a request
+-- by the Browser pipeline.
+setEventHandler :: Maybe (BrowserEvent -> BrowserAction ty ()) -> BrowserAction ty ()
+setEventHandler mbH = modify (\b -> b { bsEvent=mbH})
+
+buildBrowserEvent :: BrowserEventType -> {-URI-}String -> RequestID -> IO BrowserEvent
+buildBrowserEvent bt uri reqID = do
+  ct <- getCurrentTime
+  return BrowserEvent
+         { browserTimestamp  = ct
+         , browserRequestID  = reqID
+         , browserRequestURI = uri
+         , browserEventType  = bt
+         }
+
+reportEvent :: BrowserEventType -> {-URI-}String -> BrowserAction t ()
+reportEvent bt uri = do
+  st <- get
+  case bsEvent st of
+    Nothing -> return ()
+    Just evH -> do
+       evt <- liftIO $ buildBrowserEvent bt uri (bsRequestID st)
+       evH evt -- if it fails, we fail.
+
+-- | The default number of hops we are willing not to go beyond for
+-- request forwardings.
+defaultMaxRetries :: Int
+defaultMaxRetries = 4
+
+-- | The default number of error retries we are willing to perform.
+defaultMaxErrorRetries :: Int
+defaultMaxErrorRetries = 4
+
+-- | The default maximum HTTP Authentication attempts we will make for
+-- a single request.
+defaultMaxAuthAttempts :: Int
+defaultMaxAuthAttempts = 2
+
+-- | The default setting for auto-proxy detection.
+-- You may change this within a session via 'setAutoProxyDetect'.
+-- To avoid initial backwards compatibility issues, leave this as @False@.
+defaultAutoProxyDetect :: Bool
+defaultAutoProxyDetect = False
+
+-- | @request httpRequest@ tries to submit the 'Request' @httpRequest@
+-- to some HTTP server (possibly going via a /proxy/, see 'setProxy'.)
+-- Upon successful delivery, the URL where the response was fetched from
+-- is returned along with the 'Response' itself.
+request :: HStream ty
+        => Request ty
+        -> BrowserAction (HandleStream ty) (URI,Response ty)
+request req = nextRequest $ do
+  res <- request' nullVal initialState req
+  reportEvent ResponseFinish (show (rqURI req))
+  case res of
+    Right r -> return r
+    Left e  -> do
+     let errStr = ("Network.Browser.request: Error raised " ++ show e)
+     err errStr
+     Prelude.fail errStr
+ where
+  initialState = nullRequestState
+  nullVal      = buf_empty bufferOps
+
+-- | Internal helper function, explicitly carrying along per-request
+-- counts.
+request' :: HStream ty
+         => ty
+         -> RequestState
+         -> Request ty
+         -> BrowserAction (HandleStream ty) (Result (URI,Response ty))
+request' nullVal rqState rq = do
+   let uri = rqURI rq
+   failHTTPS uri
+   let uria = reqURIAuth rq
+     -- add cookies to request
+   cookies <- getCookiesFor (uriAuthToString uria) (uriPath uri)
+{- Not for now:
+   (case uriUserInfo uria of
+     "" -> id
+     xs ->
+       case chopAtDelim ':' xs of
+         (_,[])    -> id
+         (usr,pwd) -> withAuth
+                          AuthBasic{ auUserName = usr
+                                   , auPassword = pwd
+                                   , auRealm    = "/"
+                                   , auSite     = uri
+                                   }) $ do
+-}
+   when (not $ null cookies)
+        (out $ "Adding cookies to request.  Cookie names: "  ++ unwords (map ckName cookies))
+    -- add credentials to request
+   rq' <-
+    if not (reqStopOnDeny rqState)
+     then return rq
+     else do
+       auth <- anticipateChallenge rq
+       case auth of
+         Nothing -> return rq
+         Just x  -> return (insertHeader HdrAuthorization (withAuthority x rq) rq)
+   let rq'' = if not $ null cookies then insertHeaders [cookiesToHeader cookies] rq' else rq'
+   p <- getProxy
+   def_ua <- gets bsUserAgent
+   let defaultOpts =
+         case p of
+           NoProxy     -> defaultNormalizeRequestOptions{normUserAgent=def_ua}
+           Proxy _ ath ->
+              defaultNormalizeRequestOptions
+                { normForProxy  = True
+                , normUserAgent = def_ua
+                , normCustoms   =
+                    maybe []
+                          (\ authS -> [\ _ r -> insertHeader HdrProxyAuthorization (withAuthority authS r) r])
+                          ath
+                }
+   let final_req = normalizeRequest defaultOpts rq''
+   out ("Sending:\n" ++ show final_req)
+   e_rsp <-
+     case p of
+       NoProxy        -> dorequest (reqURIAuth rq'') final_req
+       Proxy str _ath -> do
+          let notURI
+               | null pt || null hst =
+                 URIAuth{ uriUserInfo = ""
+                        , uriRegName  = str
+                        , uriPort     = ""
+                        }
+               | otherwise =
+                 URIAuth{ uriUserInfo = ""
+                        , uriRegName  = hst
+                        , uriPort     = pt
+                        }
+                  -- If the ':' is dropped from port below, dorequest will assume port 80. Leave it!
+                 where (hst, pt) = span (':'/=) str
+           -- Proxy can take multiple forms - look for http://host:port first,
+           -- then host:port. Fall back to just the string given (probably a host name).
+          let proxyURIAuth =
+                maybe notURI
+                      (\parsed -> maybe notURI id (uriAuthority parsed))
+                      (parseURI str)
+
+          out $ "proxy uri host: " ++ uriRegName proxyURIAuth ++ ", port: " ++ uriPort proxyURIAuth
+          dorequest proxyURIAuth final_req
+   mbMx <- getMaxErrorRetries
+   case e_rsp of
+    Left v
+     | (reqRetries rqState < fromMaybe defaultMaxErrorRetries mbMx) &&
+       (v == ErrorReset || v == ErrorClosed) -> do
+       --empty connnection pool in case connection has become invalid
+       modify (\b -> b { bsConnectionPool=[] })
+       request' nullVal rqState{reqRetries=succ (reqRetries rqState)} rq
+     | otherwise ->
+       return (Left v)
+    Right rsp -> do
+     out ("Received:\n" ++ show rsp)
+      -- add new cookies to browser state
+     handleCookies uri (uriAuthToString $ reqURIAuth rq)
+                       (retrieveHeaders HdrSetCookie rsp)
+     -- Deal with "Connection: close" in response.
+     handleConnectionClose (reqURIAuth rq) (retrieveHeaders HdrConnection rsp)
+     mbMxAuths <- getMaxAuthAttempts
+     case rspCode rsp of
+      (4,0,1) -- Credentials not sent or refused.
+        | reqDenies rqState > fromMaybe defaultMaxAuthAttempts mbMxAuths -> do
+          out "401 - credentials again refused; exceeded retry count (2)"
+          return (Right (uri,rsp))
+        | otherwise -> do
+          out "401 - credentials not supplied or refused; retrying.."
+          let hdrs = retrieveHeaders HdrWWWAuthenticate rsp
+          flg <- getAllowBasicAuth
+          case pickChallenge flg (catMaybes $ map (headerToChallenge uri) hdrs) of
+            Nothing -> do
+              out "no challenge"
+              return (Right (uri,rsp))   {- do nothing -}
+            Just x  -> do
+              au <- challengeToAuthority uri x
+              case au of
+                Nothing  -> do
+                  out "no auth"
+                  return (Right (uri,rsp)) {- do nothing -}
+                Just au' -> do
+                  out "Retrying request with new credentials"
+                  request' nullVal
+                           rqState{ reqDenies     = succ(reqDenies rqState)
+                                  , reqStopOnDeny = False
+                                  }
+                           (insertHeader HdrAuthorization (withAuthority au' rq) rq)
+
+      (4,0,7)  -- Proxy Authentication required
+        | reqDenies rqState > fromMaybe defaultMaxAuthAttempts mbMxAuths -> do
+          out "407 - proxy authentication required; max deny count exceeeded (2)"
+          return (Right (uri,rsp))
+        | otherwise -> do
+          out "407 - proxy authentication required"
+          let hdrs = retrieveHeaders HdrProxyAuthenticate rsp
+          flg <- getAllowBasicAuth
+          case pickChallenge flg (catMaybes $ map (headerToChallenge uri) hdrs) of
+            Nothing -> return (Right (uri,rsp))   {- do nothing -}
+            Just x  -> do
+              au <- challengeToAuthority uri x
+              case au of
+               Nothing  -> return (Right (uri,rsp))  {- do nothing -}
+               Just au' -> do
+                 pxy <- gets bsProxy
+                 case pxy of
+                   NoProxy -> do
+                     err "Proxy authentication required without proxy!"
+                     return (Right (uri,rsp))
+                   Proxy px _ -> do
+                     out "Retrying with proxy authentication"
+                     setProxy (Proxy px (Just au'))
+                     request' nullVal
+                              rqState{ reqDenies     = succ(reqDenies rqState)
+                                     , reqStopOnDeny = False
+                                     }
+                              rq
+
+      (3,0,x) | x `elem` [2,3,1,7]  ->  do
+        out ("30" ++ show x ++  " - redirect")
+        allow_redirs <- allowRedirect rqState
+        case allow_redirs of
+          False -> return (Right (uri,rsp))
+          _ -> do
+           case retrieveHeaders HdrLocation rsp of
+            [] -> do
+              err "No Location: header in redirect response"
+              return (Right (uri,rsp))
+            (Header _ u:_) ->
+              case parseURIReference u of
+                Nothing -> do
+                  err ("Parse of Location: header in a redirect response failed: " ++ u)
+                  return (Right (uri,rsp))
+                Just newURI
+                 | {-uriScheme newURI_abs /= uriScheme uri && -}(not (supportedScheme newURI_abs)) -> do
+                    err ("Unable to handle redirect, unsupported scheme: " ++ show newURI_abs)
+                    return (Right (uri, rsp))
+                 | otherwise -> do
+                    out ("Redirecting to " ++ show newURI_abs ++ " ...")
+
+                    -- Redirect using GET request method, depending on
+                    -- response code.
+                    let toGet = x `elem` [2,3]
+                        method = if toGet then GET else rqMethod rq
+                        rq1 = rq { rqMethod=method, rqURI=newURI_abs }
+                        rq2 = if toGet then (replaceHeader HdrContentLength "0") (rq1 {rqBody = nullVal}) else rq1
+
+                    request' nullVal
+                            rqState{ reqDenies     = 0
+                                   , reqRedirects  = succ(reqRedirects rqState)
+                                   , reqStopOnDeny = True
+                                   }
+                             rq2
+                 where
+                   newURI_abs = uriDefaultTo newURI uri
+
+      (3,0,5) ->
+        case retrieveHeaders HdrLocation rsp of
+         [] -> do
+           err "No Location header in proxy redirect response."
+           return (Right (uri,rsp))
+         (Header _ u:_) ->
+           case parseURIReference u of
+            Nothing -> do
+             err ("Parse of Location header in a proxy redirect response failed: " ++ u)
+             return (Right (uri,rsp))
+            Just newuri -> do
+             out ("Retrying with proxy " ++ show newuri ++ "...")
+             setProxy (Proxy (uriToAuthorityString newuri) Nothing)
+             request' nullVal rqState{ reqDenies     = 0
+                                     , reqRedirects  = 0
+                                     , reqRetries    = succ (reqRetries rqState)
+                                     , reqStopOnDeny = True
+                                     }
+                                     rq
+      _       -> return (Right (uri,rsp))
+
+-- | The internal request handling state machine.
+dorequest :: (HStream ty)
+          => URIAuth
+          -> Request ty
+          -> BrowserAction (HandleStream ty)
+                           (Result (Response ty))
+dorequest hst rqst = do
+  pool <- gets bsConnectionPool
+  let uPort = uriAuthPort Nothing{-ToDo: feed in complete URL-} hst
+  conn <- liftIO $ filterM (\c -> c `isTCPConnectedTo` EndPoint (uriRegName hst) uPort) pool
+  rsp <-
+    case conn of
+      [] -> do
+        out ("Creating new connection to " ++ uriAuthToString hst)
+        reportEvent OpenConnection (show (rqURI rqst))
+        c <- liftIO $ openStream (uriRegName hst) uPort
+        updateConnectionPool c
+        dorequest2 c rqst
+      (c:_) -> do
+        out ("Recovering connection to " ++ uriAuthToString hst)
+        reportEvent ReuseConnection (show (rqURI rqst))
+        dorequest2 c rqst
+  case rsp of
+     Right (Response a b c _) ->
+         reportEvent (ResponseEnd (a,b,c)) (show (rqURI rqst)) ; _ -> return ()
+  return rsp
+ where
+  dorequest2 c r = do
+    dbg <- gets bsDebug
+    st  <- get
+    let
+     onSendComplete =
+       maybe (return ())
+             (\evh -> do
+                x <- buildBrowserEvent RequestSent (show (rqURI r)) (bsRequestID st)
+                runBA st (evh x)
+                return ())
+             (bsEvent st)
+    liftIO $
+      maybe (sendHTTP_notify c r onSendComplete)
+            (\ f -> do
+               c' <- debugByteStream (f++'-': uriAuthToString hst) c
+               sendHTTP_notify c' r onSendComplete)
+            dbg
+
+updateConnectionPool :: HStream hTy
+                     => HandleStream hTy
+                     -> BrowserAction (HandleStream hTy) ()
+updateConnectionPool c = do
+   pool <- gets bsConnectionPool
+   let len_pool = length pool
+   maxPoolSize <- fromMaybe defaultMaxPoolSize <$> gets bsMaxPoolSize
+   when (len_pool > maxPoolSize)
+        (liftIO $ close (last pool))
+   let pool'
+        | len_pool > maxPoolSize = init pool
+        | otherwise              = pool
+   when (maxPoolSize > 0) $ modify (\b -> b { bsConnectionPool=c:pool' })
+   return ()
+
+-- | Default maximum number of open connections we are willing to have active.
+defaultMaxPoolSize :: Int
+defaultMaxPoolSize = 5
+
+cleanConnectionPool :: HStream hTy
+                    => URIAuth -> BrowserAction (HandleStream hTy) ()
+cleanConnectionPool uri = do
+  let ep = EndPoint (uriRegName uri) (uriAuthPort Nothing uri)
+  pool <- gets bsConnectionPool
+  bad <- liftIO $ mapM (\c -> c `isTCPConnectedTo` ep) pool
+  let tmp = zip bad pool
+      newpool = map snd $ filter (not . fst) tmp
+      toclose = map snd $ filter fst tmp
+  liftIO $ forM_ toclose close
+  modify (\b -> b { bsConnectionPool = newpool })
+
+handleCookies :: URI -> String -> [Header] -> BrowserAction t ()
+handleCookies _   _              [] = return () -- cut short the silliness.
+handleCookies uri dom cookieHeaders = do
+  when (not $ null errs)
+       (err $ unlines ("Errors parsing these cookie values: ":errs))
+  when (not $ null newCookies)
+       (out $ foldl (\x y -> x ++ "\n  " ++ show y) "Cookies received:" newCookies)
+  filterfn    <- getCookieFilter
+  newCookies' <- liftIO (filterM (filterfn uri) newCookies)
+  when (not $ null newCookies')
+       (out $ "Accepting cookies with names: " ++ unwords (map ckName newCookies'))
+  mapM_ addCookie newCookies'
+ where
+  (errs, newCookies) = processCookieHeaders dom cookieHeaders
+
+handleConnectionClose :: HStream hTy
+                      => URIAuth -> [Header]
+                      -> BrowserAction (HandleStream hTy) ()
+handleConnectionClose _ [] = return ()
+handleConnectionClose uri headers = do
+  let doClose = any (== "close") $ map headerToConnType headers
+  when doClose $ cleanConnectionPool uri
+  where headerToConnType (Header _ t) = map toLower t
+
+------------------------------------------------------------------
+----------------------- Miscellaneous ----------------------------
+------------------------------------------------------------------
+
+allowRedirect :: RequestState -> BrowserAction t Bool
+allowRedirect rqState = do
+  rd <- getAllowRedirects
+  mbMxRetries <- getMaxRedirects
+  return (rd && (reqRedirects rqState <= fromMaybe defaultMaxRetries mbMxRetries))
+
+-- | Return @True@ iff the package is able to handle requests and responses
+-- over it.
+supportedScheme :: URI -> Bool
+supportedScheme u = uriScheme u == "http:"
+
+-- | @uriDefaultTo a b@ returns a URI that is consistent with the first
+-- argument URI @a@ when read in the context of the second URI @b@.
+-- If the second argument is not sufficient context for determining
+-- a full URI then anarchy reins.
+uriDefaultTo :: URI -> URI -> URI
+#if MIN_VERSION_network(2,4,0)
+uriDefaultTo a b = a `relativeTo` b
+#else
+uriDefaultTo a b = maybe a id (a `relativeTo` b)
+#endif
+
+
+-- This form junk is completely untested...
+
+type FormVar = (String,String)
+
+data Form = Form RequestMethod URI [FormVar]
+
+formToRequest :: Form -> Request_String
+formToRequest (Form m u vs) =
+    let enc = urlEncodeVars vs
+    in case m of
+        GET -> Request { rqMethod=GET
+                       , rqHeaders=[ Header HdrContentLength "0" ]
+                       , rqBody=""
+                       , rqURI=u { uriQuery= '?' : enc }  -- What about old query?
+                       }
+        POST -> Request { rqMethod=POST
+                        , rqHeaders=[ Header HdrContentType "application/x-www-form-urlencoded",
+                                      Header HdrContentLength (show $ length enc) ]
+                        , rqBody=enc
+                        , rqURI=u
+                        }
+        _ -> error ("unexpected request: " ++ show m)
+
diff --git a/Network/BufferType.hs b/Network/BufferType.hs
new file mode 100644
--- /dev/null
+++ b/Network/BufferType.hs
@@ -0,0 +1,164 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.BufferType
+-- Description :  Abstract representation of request and response buffer types.
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- In order to give the user freedom in how request and response content
+-- is represented, a sufficiently abstract representation is needed of
+-- these internally. The "Network.BufferType" module provides this, defining
+-- the 'BufferType' class and its ad-hoc representation of buffer operations
+-- via the 'BufferOp' record.
+--
+-- This module provides definitions for the standard buffer types that the
+-- package supports, i.e., for @String@ and @ByteString@ (strict and lazy.)
+--
+-----------------------------------------------------------------------------
+module Network.BufferType
+       (
+         BufferType(..)
+
+       , BufferOp(..)
+       , strictBufferOp
+       , lazyBufferOp
+       , stringBufferOp
+       ) where
+
+
+import qualified Data.ByteString       as Strict hiding ( unpack, pack, span )
+import qualified Data.ByteString.Char8 as Strict ( unpack, pack, span )
+import qualified Data.ByteString.Lazy as Lazy hiding ( pack, unpack,span )
+import qualified Data.ByteString.Lazy.Char8 as Lazy ( pack, unpack, span )
+import System.IO ( Handle )
+import Data.Word ( Word8 )
+
+import Network.HTTP.Utils ( crlf, lf )
+
+-- | The @BufferType@ class encodes, in a mixed-mode way, the interface
+-- that the library requires to operate over data embedded in HTTP
+-- requests and responses. That is, we use explicit dictionaries
+-- for the operations, but overload the name of the dicts themselves.
+--
+class BufferType bufType where
+   bufferOps :: BufferOp bufType
+
+instance BufferType Lazy.ByteString where
+   bufferOps = lazyBufferOp
+
+instance BufferType Strict.ByteString where
+   bufferOps = strictBufferOp
+
+instance BufferType String where
+   bufferOps = stringBufferOp
+
+-- | @BufferOp@ encodes the I/O operations of the underlying buffer over
+-- a Handle in an (explicit) dictionary type. May not be needed, but gives
+-- us flexibility in explicit overriding and wrapping up of these methods.
+--
+-- Along with IO operations is an ad-hoc collection of functions for working
+-- with these abstract buffers, as needed by the internals of the code
+-- that processes requests and responses.
+--
+-- We supply three default @BufferOp@ values, for @String@ along with the
+-- strict and lazy versions of @ByteString@. To add others, provide @BufferOp@
+-- definitions for
+data BufferOp a
+ = BufferOp
+     { buf_hGet         :: Handle -> Int -> IO a
+     , buf_hGetContents :: Handle -> IO a
+     , buf_hPut         :: Handle -> a   -> IO ()
+     , buf_hGetLine     :: Handle -> IO a
+     , buf_empty        :: a
+     , buf_append       :: a -> a -> a
+     , buf_concat       :: [a] -> a
+     , buf_fromStr      :: String -> a
+     , buf_toStr        :: a -> String
+     , buf_snoc         :: a -> Word8 -> a
+     , buf_splitAt      :: Int -> a -> (a,a)
+     , buf_span         :: (Char  -> Bool) -> a -> (a,a)
+     , buf_isLineTerm   :: a -> Bool
+     , buf_isEmpty      :: a -> Bool
+     }
+
+instance Eq (BufferOp a) where
+  _ == _ = False
+
+-- | @strictBufferOp@ is the 'BufferOp' definition over @ByteString@s,
+-- the non-lazy kind.
+strictBufferOp :: BufferOp Strict.ByteString
+strictBufferOp =
+    BufferOp
+      { buf_hGet         = Strict.hGet
+      , buf_hGetContents = Strict.hGetContents
+      , buf_hPut         = Strict.hPut
+      , buf_hGetLine     = Strict.hGetLine
+      , buf_append       = Strict.append
+      , buf_concat       = Strict.concat
+      , buf_fromStr      = Strict.pack
+      , buf_toStr        = Strict.unpack
+      , buf_snoc         = Strict.snoc
+      , buf_splitAt      = Strict.splitAt
+      , buf_span         = Strict.span
+      , buf_empty        = Strict.empty
+      , buf_isLineTerm   = \ b -> Strict.length b == 2 && p_crlf == b ||
+                                  Strict.length b == 1 && p_lf   == b
+      , buf_isEmpty      = Strict.null
+      }
+   where
+    p_crlf = Strict.pack crlf
+    p_lf   = Strict.pack lf
+
+-- | @lazyBufferOp@ is the 'BufferOp' definition over @ByteString@s,
+-- the non-strict kind.
+lazyBufferOp :: BufferOp Lazy.ByteString
+lazyBufferOp =
+    BufferOp
+      { buf_hGet         = Lazy.hGet
+      , buf_hGetContents = Lazy.hGetContents
+      , buf_hPut         = Lazy.hPut
+      , buf_hGetLine     = \ h -> Strict.hGetLine h >>= \ l -> return (Lazy.fromChunks [l])
+      , buf_append       = Lazy.append
+      , buf_concat       = Lazy.concat
+      , buf_fromStr      = Lazy.pack
+      , buf_toStr        = Lazy.unpack
+      , buf_snoc         = Lazy.snoc
+      , buf_splitAt      = \ i x -> Lazy.splitAt (fromIntegral i) x
+      , buf_span         = Lazy.span
+      , buf_empty        = Lazy.empty
+      , buf_isLineTerm   = \ b -> Lazy.length b == 2 && p_crlf == b ||
+                                  Lazy.length b == 1 && p_lf   == b
+      , buf_isEmpty      = Lazy.null
+      }
+   where
+    p_crlf = Lazy.pack crlf
+    p_lf   = Lazy.pack lf
+
+-- | @stringBufferOp@ is the 'BufferOp' definition over @String@s.
+-- It is defined in terms of @strictBufferOp@ operations,
+-- unpacking/converting to @String@ when needed.
+stringBufferOp :: BufferOp String
+stringBufferOp =BufferOp
+      { buf_hGet         = \ h n -> buf_hGet strictBufferOp h n >>= return . Strict.unpack
+      , buf_hGetContents = \ h -> buf_hGetContents strictBufferOp h >>= return . Strict.unpack
+      , buf_hPut         = \ h s -> buf_hPut strictBufferOp h (Strict.pack s)
+      , buf_hGetLine     = \ h   -> buf_hGetLine strictBufferOp h >>= return . Strict.unpack
+      , buf_append       = (++)
+      , buf_concat       = concat
+      , buf_fromStr      = id
+      , buf_toStr        = id
+      , buf_snoc         = \ a x -> a ++ [toEnum (fromIntegral x)]
+      , buf_splitAt      = splitAt
+      , buf_span         = \ p a ->
+                             case Strict.span p (Strict.pack a) of
+                               (x,y) -> (Strict.unpack x, Strict.unpack y)
+      , buf_empty        = []
+      , buf_isLineTerm   = \ b -> b == crlf || b == lf
+      , buf_isEmpty      = null
+      }
+
diff --git a/Network/HTTP.hs b/Network/HTTP.hs
--- a/Network/HTTP.hs
+++ b/Network/HTTP.hs
@@ -1,1052 +1,265 @@
 -----------------------------------------------------------------------------
 -- |
 -- Module      :  Network.HTTP
--- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2005
--- License     :  BSD
--- 
--- Maintainer  :  bjorn@bringert.net
--- Stability   :  experimental
--- Portability :  non-portable (not tested)
---
--- An easy HTTP interface enjoy.
---
--- * Changes by Simon Foster:
---      - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules
---      - Created functions receiveHTTP and responseHTTP to allow server side interactions
---        (although 100-continue is unsupported and I haven't checked for standard compliancy).
---      - Pulled the transfer functions from sendHTTP to global scope to allow access by
---        above functions.
---
--- * Changes by Graham Klyne:
---      - export httpVersion
---      - use new URI module (similar to old, but uses revised URI datatype)
---
--- * Changes by Bjorn Bringert:
---
---      - handle URIs with a port number
---      - added debugging toggle
---      - disabled 100-continue transfers to get HTTP\/1.0 compatibility
---      - change 'ioError' to 'throw'
---      - Added simpleHTTP_, which takes a stream argument.
---
--- * Changes from 0.1
---      - change 'openHTTP' to 'openTCP', removed 'closeTCP' - use 'close' from 'Stream' class.
---      - added use of inet_addr to openHTTP, allowing use of IP "dot" notation addresses.
---      - reworking of the use of Stream, including alterations to make 'sendHTTP' generic
---        and the addition of a debugging stream.
---      - simplified error handling.
--- 
--- * TODO
---     - request pipelining
---     - https upgrade (includes full TLS, i.e. SSL, implementation)
---         - use of Stream classes will pay off
---         - consider C implementation of encryption\/decryption
---     - comm timeouts
---     - MIME & entity stuff (happening in separate module)
---     - support \"*\" uri-request-string for OPTIONS request method
--- 
--- 
--- * Header notes:
---
---     [@Host@]
---                  Required by HTTP\/1.1, if not supplied as part
---                  of a request a default Host value is extracted
---                  from the request-uri.
--- 
---     [@Connection@] 
---                  If this header is present in any request or
---                  response, and it's value is "close", then
---                  the current request\/response is the last 
---                  to be allowed on that connection.
--- 
---     [@Expect@]
---                  Should a request contain a body, an Expect
---                  header will be added to the request.  The added
---                  header has the value \"100-continue\".  After
---                  a 417 \"Expectation Failed\" response the request
---                  is attempted again without this added Expect
---                  header.
---                  
---     [@TransferEncoding,ContentLength,...@]
---                  if request is inconsistent with any of these
---                  header values then you may not receive any response
---                  or will generate an error response (probably 4xx).
---
---
--- * Response code notes
--- Some response codes induce special behaviour:
---
---   [@1xx@]   \"100 Continue\" will cause any unsent request body to be sent.
---             \"101 Upgrade\" will be returned.
---             Other 1xx responses are ignored.
--- 
---   [@417@]   The reason for this code is \"Expectation failed\", indicating
---             that the server did not like the Expect \"100-continue\" header
---             added to a request.  Receipt of 417 will induce another
---             request attempt (without Expect header), unless no Expect header
---             had been added (in which case 417 response is returned).
---
------------------------------------------------------------------------------
-module Network.HTTP (
-    module Network.Stream,
-    module Network.TCP,
-
-    -- ** Constants
-    httpVersion,
-    
-    -- ** HTTP 
-    Request(..),
-    Response(..),
-    RequestMethod(..),
-    simpleHTTP, simpleHTTP_,
-    sendHTTP,
-    receiveHTTP,
-    respondHTTP,
-
-    -- ** Header Functions
-    HasHeaders,
-    Header(..),
-    HeaderName(..),
-    insertHeader,
-    insertHeaderIfMissing,
-    insertHeaders,
-    retrieveHeaders,
-    replaceHeader,
-    findHeader,
-
-    -- ** URL Encoding
-    urlEncode,
-    urlDecode,
-    urlEncodeVars,
-
-    -- ** URI authority parsing
-    URIAuthority(..),
-    parseURIAuthority
-) where
-
-
-
------------------------------------------------------------------
------------------- Imports --------------------------------------
------------------------------------------------------------------
-
-import Control.Exception as Exception
-
--- Networking
-import Network (withSocketsDo)
-import Network.BSD
-import Network.URI
-import Network.Socket
-import Network.Stream
-import Network.TCP
-
-
--- Util
-import Data.Bits ((.&.))
-import Data.Char
-import Data.List (isPrefixOf,partition,elemIndex)
-import Data.Maybe
-import Data.Array.MArray
-import Data.IORef
-import Control.Concurrent
-import Control.Monad (when,liftM,guard)
-import Control.Monad.ST (ST,stToIO)
-import Numeric (readHex)
-import Text.ParserCombinators.ReadP
-import Text.Read.Lex 
-import System.IO
-import System.IO.Error (isEOFError)
-import qualified System.IO.Error
-
-import Foreign.C.Error
-
-
--- Turn on to enable HTTP traffic logging
-debug :: Bool
-debug = False
-
--- File that HTTP traffic logs go to
-httpLogFile :: String
-httpLogFile = "http-debug.log"
-
------------------------------------------------------------------
------------------- Misc -----------------------------------------
------------------------------------------------------------------
-
--- remove leading and trailing whitespace.
-trim :: String -> String
-trim = let dropspace = dropWhile isSpace in
-       reverse . dropspace . reverse . dropspace
-
-
--- Split a list into two parts, the delimiter occurs
--- at the head of the second list.  Nothing is returned
--- when no occurance of the delimiter is found.
-split :: Eq a => a -> [a] -> Maybe ([a],[a])
-split delim list = case delim `elemIndex` list of
-    Nothing -> Nothing
-    Just x  -> Just $ splitAt x list
-    
-
-
-crlf = "\r\n"
-sp   = " "
-
------------------------------------------------------------------
------------------- URI Authority parsing ------------------------
------------------------------------------------------------------
-
-data URIAuthority = URIAuthority { user :: Maybe String, 
-				   password :: Maybe String,
-				   host :: String,
-				   port :: Maybe Int
-				 } deriving (Eq,Show)
-
--- | Parse the authority part of a URL.
---
--- > RFC 1732, section 3.1:
--- >
--- >       //<user>:<password>@<host>:<port>/<url-path>
--- >  Some or all of the parts "<user>:<password>@", ":<password>",
--- >  ":<port>", and "/<url-path>" may be excluded.
-parseURIAuthority :: String -> Maybe URIAuthority
-parseURIAuthority s = listToMaybe (map fst (readP_to_S pURIAuthority s))
-
-
-pURIAuthority :: ReadP URIAuthority
-pURIAuthority = do
-		(u,pw) <- (pUserInfo `before` char '@') 
-			  <++ return (Nothing, Nothing)
-		h <- munch (/=':')
-		p <- orNothing (char ':' >> readDecP)
-		look >>= guard . null 
-		return URIAuthority{ user=u, password=pw, host=h, port=p }
-
-pUserInfo :: ReadP (Maybe String, Maybe String)
-pUserInfo = do
-	    u <- orNothing (munch (`notElem` ":@"))
-	    p <- orNothing (char ':' >> munch (/='@'))
-	    return (u,p)
-
-before :: Monad m => m a -> m b -> m a
-before a b = a >>= \x -> b >> return x
-
-orNothing :: ReadP a -> ReadP (Maybe a)
-orNothing p = fmap Just p <++ return Nothing
-
------------------------------------------------------------------
------------------- Header Data ----------------------------------
------------------------------------------------------------------
-
-
--- | The Header data type pairs header names & values.
-data Header = Header HeaderName String
-
-
-instance Show Header where
-    show (Header key value) = show key ++ ": " ++ value ++ crlf
-
-
--- | HTTP Header Name type:
---  Why include this at all?  I have some reasons
---   1) prevent spelling errors of header names,
---   2) remind everyone of what headers are available,
---   3) might speed up searches for specific headers.
---
---  Arguments against:
---   1) makes customising header names laborious
---   2) increases code volume.
---
-data HeaderName = 
-                 -- Generic Headers --
-                  HdrCacheControl
-                | HdrConnection
-                | HdrDate
-                | HdrPragma
-                | HdrTransferEncoding        
-                | HdrUpgrade                
-                | HdrVia
-
-                -- Request Headers --
-                | HdrAccept
-                | HdrAcceptCharset
-                | HdrAcceptEncoding
-                | HdrAcceptLanguage
-                | HdrAuthorization
-                | HdrCookie
-                | HdrExpect
-                | HdrFrom
-                | HdrHost
-                | HdrIfModifiedSince
-                | HdrIfMatch
-                | HdrIfNoneMatch
-                | HdrIfRange
-                | HdrIfUnmodifiedSince
-                | HdrMaxForwards
-                | HdrProxyAuthorization
-                | HdrRange
-                | HdrReferer
-                | HdrUserAgent
-
-                -- Response Headers
-                | HdrAge
-                | HdrLocation
-                | HdrProxyAuthenticate
-                | HdrPublic
-                | HdrRetryAfter
-                | HdrServer
-                | HdrSetCookie
-                | HdrVary
-                | HdrWarning
-                | HdrWWWAuthenticate
-
-                -- Entity Headers
-                | HdrAllow
-                | HdrContentBase
-                | HdrContentEncoding
-                | HdrContentLanguage
-                | HdrContentLength
-                | HdrContentLocation
-                | HdrContentMD5
-                | HdrContentRange
-                | HdrContentType
-                | HdrETag
-                | HdrExpires
-                | HdrLastModified
-
-                -- Mime entity headers (for sub-parts)
-                | HdrContentTransferEncoding
-
-                -- | Allows for unrecognised or experimental headers.
-                | HdrCustom String -- not in header map below.
-    deriving(Eq)
-
-
--- Translation between header names and values,
--- good candidate for improvement.
-headerMap :: [ (String,HeaderName) ]
-headerMap 
- = [  ("Cache-Control"        ,HdrCacheControl      )
-	, ("Connection"           ,HdrConnection        )
-	, ("Date"                 ,HdrDate              )    
-	, ("Pragma"               ,HdrPragma            )
-	, ("Transfer-Encoding"    ,HdrTransferEncoding  )        
-	, ("Upgrade"              ,HdrUpgrade           )                
-	, ("Via"                  ,HdrVia               )
-	, ("Accept"               ,HdrAccept            )
-	, ("Accept-Charset"       ,HdrAcceptCharset     )
-	, ("Accept-Encoding"      ,HdrAcceptEncoding    )
-	, ("Accept-Language"      ,HdrAcceptLanguage    )
-	, ("Authorization"        ,HdrAuthorization     )
-	, ("From"                 ,HdrFrom              )
-	, ("Host"                 ,HdrHost              )
-	, ("If-Modified-Since"    ,HdrIfModifiedSince   )
-	, ("If-Match"             ,HdrIfMatch           )
-	, ("If-None-Match"        ,HdrIfNoneMatch       )
-	, ("If-Range"             ,HdrIfRange           ) 
-	, ("If-Unmodified-Since"  ,HdrIfUnmodifiedSince )
-	, ("Max-Forwards"         ,HdrMaxForwards       )
-	, ("Proxy-Authorization"  ,HdrProxyAuthorization)
-	, ("Range"                ,HdrRange             )   
-	, ("Referer"              ,HdrReferer           )
-	, ("User-Agent"           ,HdrUserAgent         )
-	, ("Age"                  ,HdrAge               )
-	, ("Location"             ,HdrLocation          )
-	, ("Proxy-Authenticate"   ,HdrProxyAuthenticate )
-	, ("Public"               ,HdrPublic            )
-	, ("Retry-After"          ,HdrRetryAfter        )
-	, ("Server"               ,HdrServer            )
-	, ("Vary"                 ,HdrVary              )
-	, ("Warning"              ,HdrWarning           )
-	, ("WWW-Authenticate"     ,HdrWWWAuthenticate   )
-	, ("Allow"                ,HdrAllow             )
-	, ("Content-Base"         ,HdrContentBase       )
-	, ("Content-Encoding"     ,HdrContentEncoding   )
-	, ("Content-Language"     ,HdrContentLanguage   )
-	, ("Content-Length"       ,HdrContentLength     )
-	, ("Content-Location"     ,HdrContentLocation   )
-	, ("Content-MD5"          ,HdrContentMD5        )
-	, ("Content-Range"        ,HdrContentRange      )
-	, ("Content-Type"         ,HdrContentType       )
-	, ("ETag"                 ,HdrETag              )
-	, ("Expires"              ,HdrExpires           )
-	, ("Last-Modified"        ,HdrLastModified      )
-   	, ("Set-Cookie"           ,HdrSetCookie         )
-	, ("Cookie"               ,HdrCookie            )
-    , ("Expect"               ,HdrExpect            ) ]
-
-
-instance Show HeaderName where
-    show (HdrCustom s) = s
-    show x = case filter ((==x).snd) headerMap of
-                [] -> error "headerMap incomplete"
-                (h:_) -> fst h
-
-
-
-
-
--- | This class allows us to write generic header manipulation functions
--- for both 'Request' and 'Response' data types.
-class HasHeaders x where
-    getHeaders :: x -> [Header]
-    setHeaders :: x -> [Header] -> x
-
-
-
--- Header manipulation functions
-insertHeader, replaceHeader, insertHeaderIfMissing
-    :: HasHeaders a => HeaderName -> String -> a -> a
-
-
--- | Inserts a header with the given name and value.
--- Allows duplicate header names.
-insertHeader name value x = setHeaders x newHeaders
-    where
-        newHeaders = (Header name value) : getHeaders x
-
-
--- | Adds the new header only if no previous header shares
--- the same name.
-insertHeaderIfMissing name value x = setHeaders x (newHeaders $ getHeaders x)
-    where
-        newHeaders list@(h@(Header n _): rest)
-            | n == name  = list
-            | otherwise  = h : newHeaders rest
-        newHeaders [] = [Header name value]
-
-            
-
--- | Removes old headers with duplicate name.
-replaceHeader name value x = setHeaders x newHeaders
-    where
-        newHeaders = Header name value : [ x | x@(Header n v) <- getHeaders x, name /= n ]
-          
-
--- | Inserts multiple headers.
-insertHeaders :: HasHeaders a => [Header] -> a -> a
-insertHeaders hdrs x = setHeaders x (getHeaders x ++ hdrs)
-
-
--- | Gets a list of headers with a particular 'HeaderName'.
-retrieveHeaders :: HasHeaders a => HeaderName -> a -> [Header]
-retrieveHeaders name x = filter matchname (getHeaders x)
-    where
-        matchname (Header n _)  |  n == name  =  True
-        matchname _ = False
-
-
--- | Lookup presence of specific HeaderName in a list of Headers
--- Returns the value from the first matching header.
-findHeader :: HasHeaders a => HeaderName -> a -> Maybe String
-findHeader n x = lookupHeader n (getHeaders x)
-
--- An anomally really:
-lookupHeader :: HeaderName -> [Header] -> Maybe String
-lookupHeader v (Header n s:t)  |  v == n   =  Just s
-                               | otherwise =  lookupHeader v t
-lookupHeader _ _  =  Nothing
-
-
-
-
-{-
-instance HasHeaders [Header]
-...requires -fglasgow-exts, and is not really necessary anyway...
--}
-
-
-
------------------------------------------------------------------
------------------- HTTP Messages --------------------------------
------------------------------------------------------------------
-
-
--- Protocol version
-httpVersion :: String
-httpVersion = "HTTP/1.1"
-
-
--- | The HTTP request method, to be used in the 'Request' object.
--- We are missing a few of the stranger methods, but these are
--- not really necessary until we add full TLS.
-data RequestMethod = HEAD | PUT | GET | POST | OPTIONS | TRACE
-    deriving(Show,Eq)
-
-rqMethodMap = [("HEAD",    HEAD),
-	       ("PUT",     PUT),
-	       ("GET",     GET),
-	       ("POST",    POST),
-	       ("OPTIONS", OPTIONS),
-	       ("TRACE",   TRACE)]
-
--- | An HTTP Request.
--- The 'Show' instance of this type is used for message serialisation,
--- which means no body data is output.
-data Request =
-     Request { rqURI       :: URI   -- ^ might need changing in future
-                                    --  1) to support '*' uri in OPTIONS request
-                                    --  2) transparent support for both relative
-                                    --     & absolute uris, although this should
-                                    --     already work (leave scheme & host parts empty).
-             , rqMethod    :: RequestMethod             
-             , rqHeaders   :: [Header]
-             , rqBody      :: String
-             }
-
-
-
-
--- Notice that request body is not included,
--- this show function is used to serialise
--- a request for the transport link, we send
--- the body separately where possible.
-instance Show Request where
-    show (Request u m h _) =
-        show m ++ sp ++ alt_uri ++ sp ++ httpVersion ++ crlf
-        ++ foldr (++) [] (map show h) ++ crlf
-        where
-            alt_uri = show $ if null (uriPath u) || head (uriPath u) /= '/' 
-                        then u { uriPath = '/' : uriPath u } 
-                        else u
-
-
-instance HasHeaders Request where
-    getHeaders = rqHeaders
-    setHeaders rq hdrs = rq { rqHeaders=hdrs }
-
-
-
-
-
-
-type ResponseCode  = (Int,Int,Int)
-type ResponseData  = (ResponseCode,String,[Header])
-type RequestData   = (RequestMethod,URI,[Header])
-
--- | An HTTP Response.
--- The 'Show' instance of this type is used for message serialisation,
--- which means no body data is output, additionally the output will
--- show an HTTP version of 1.1 instead of the actual version returned
--- by a server.
-data Response =
-    Response { rspCode     :: ResponseCode
-             , rspReason   :: String
-             , rspHeaders  :: [Header]
-             , rspBody     :: String
-             }
-                   
-
-
--- This is an invalid representation of a received response, 
--- since we have made the assumption that all responses are HTTP/1.1
-instance Show Response where
-    show (Response (a,b,c) reason headers _) =
-        httpVersion ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf
-        ++ foldr (++) [] (map show headers) ++ crlf
-
-
-
-instance HasHeaders Response where
-    getHeaders = rspHeaders
-    setHeaders rsp hdrs = rsp { rspHeaders=hdrs }
-
------------------------------------------------------------------
------------------- Parsing --------------------------------------
------------------------------------------------------------------
-
-parseHeader :: String -> Result Header
-parseHeader str =
-    case split ':' str of
-        Nothing -> Left (ErrorParse $ "Unable to parse header: " ++ str)
-        Just (k,v) -> Right $ Header (fn k) (trim $ drop 1 v)
-    where
-        fn k = case map snd $ filter (match k . fst) headerMap of
-                 [] -> (HdrCustom k)
-                 (h:_) -> h
-
-        match :: String -> String -> Bool
-        match s1 s2 = map toLower s1 == map toLower s2
-    
-
-parseHeaders :: [String] -> Result [Header]
-parseHeaders = catRslts [] . map (parseHeader . clean) . joinExtended ""
-    where
-        -- Joins consecutive lines where the second line
-        -- begins with ' ' or '\t'.
-        joinExtended old (h : t)
-            | not (null h) && (head h == ' ' || head h == '\t')
-                = joinExtended (old ++ ' ' : tail h) t
-            | otherwise = old : joinExtended h t
-        joinExtended old [] = [old]
-
-        clean [] = []
-        clean (h:t) | h `elem` "\t\r\n" = ' ' : clean t
-                    | otherwise = h : clean t
-
-        -- tollerant of errors?  should parse
-        -- errors here be reported or ignored?
-        -- currently ignored.
-        catRslts :: [a] -> [Result a] -> Result [a]
-        catRslts list (h:t) = 
-            case h of
-                Left _ -> catRslts list t
-                Right v -> catRslts (v:list) t
-        catRslts list [] = Right $ reverse list            
-        
-
--- Parsing a request
-parseRequestHead :: [String] -> Result RequestData
-parseRequestHead [] = Left ErrorClosed
-parseRequestHead (com:hdrs) =
-    requestCommand com `bindE` \(version,rqm,uri) ->
-    parseHeaders hdrs `bindE` \hdrs' ->
-    Right (rqm,uri,hdrs')
-    where
-        requestCommand line
-	    =  case words line of
-                yes@(rqm:uri:version) -> case (parseURIReference uri, lookup rqm rqMethodMap) of
-					  (Just u, Just r) -> Right (version,r,u)
-					  _                -> Left (ErrorParse $ "Request command line parse failure: " ++ line)
-		no -> if null line
-			       then Left ErrorClosed
-			       else Left (ErrorParse $ "Request command line parse failure: " ++ line)  
-
--- Parsing a response
-parseResponseHead :: [String] -> Result ResponseData
-parseResponseHead [] = Left ErrorClosed
-parseResponseHead (sts:hdrs) = 
-    responseStatus sts `bindE` \(version,code,reason) ->
-    parseHeaders hdrs `bindE` \hdrs' ->
-    Right (code,reason,hdrs')
-    where
-
-        responseStatus line
-            =  case words line of
-                yes@(version:code:reason) -> Right (version,match code,concatMap (++" ") reason)
-                no -> if null line 
-                    then Left ErrorClosed  -- an assumption
-                    else Left (ErrorParse $ "Response status line parse failure: " ++ line)
-
-
-        match [a,b,c] = (digitToInt a,
-                         digitToInt b,
-                         digitToInt c)
-        match _ = (-1,-1,-1)  -- will create appropriate behaviour
-
-
-        
-
------------------------------------------------------------------
------------------- HTTP Send / Recv ----------------------------------
------------------------------------------------------------------
-
-data Behaviour = Continue
-               | Retry
-               | Done
-               | ExpectEntity
-               | DieHorribly String
-
-
-
-
-
-matchResponse :: RequestMethod -> ResponseCode -> Behaviour
-matchResponse rqst rsp =
-    case rsp of
-        (1,0,0) -> Continue
-        (1,0,1) -> Done        -- upgrade to TLS
-        (1,_,_) -> Continue    -- default
-        (2,0,4) -> Done
-        (2,0,5) -> Done
-        (2,_,_) -> ans
-        (3,0,4) -> Done
-        (3,0,5) -> Done
-        (3,_,_) -> ans
-        (4,1,7) -> Retry       -- Expectation failed
-        (4,_,_) -> ans
-        (5,_,_) -> ans
-        (a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised")
-    where
-        ans | rqst == HEAD = Done
-            | otherwise    = ExpectEntity
-        
-
--- | Simple way to get a resource across a non-persistant connection.
--- Headers that may be altered:
---  Host        Altered only if no Host header is supplied, HTTP\/1.1
---              requires a Host header.
---  Connection  Where no allowance is made for persistant connections
---              the Connection header will be set to "close"
-simpleHTTP :: Request -> IO (Result Response)
-simpleHTTP r = 
-    do 
-       auth <- getAuth r
-       c <- openTCPPort (host auth) (fromMaybe 80 (port auth))
-       simpleHTTP_ c r
-
--- | Like 'simpleHTTP', but acting on an already opened stream.
-simpleHTTP_ :: Stream s => s -> Request -> IO (Result Response)
-simpleHTTP_ s r =
-    do 
-       auth <- getAuth r
-       let r' = fixReq auth r 
-       rsp <- if debug then do
-	        s' <- debugStream httpLogFile s
-	        sendHTTP s' r'
-	       else
-	        sendHTTP s r'
-       -- already done by sendHTTP because of "Connection: close" header
-       --; close s 
-       return rsp
-       where
-  {- RFC 2616, section 5.1.2:
-     "The most common form of Request-URI is that used to identify a
-      resource on an origin server or gateway. In this case the absolute
-      path of the URI MUST be transmitted (see section 3.2.1, abs_path) as
-      the Request-URI, and the network location of the URI (authority) MUST
-      be transmitted in a Host header field." -}
-  -- we assume that this is the case, so we take the host name from
-  -- the Host header if there is one, otherwise from the request-URI.
-  -- Then we make the request-URI an abs_path and make sure that there
-  -- is a Host header.
-             fixReq :: URIAuthority -> Request -> Request
-	     fixReq URIAuthority{host=h} r = 
-		 replaceHeader HdrConnection "close" $
-		 insertHeaderIfMissing HdrHost h $
-		 r { rqURI = (rqURI r){ uriScheme = "", 
-					uriAuthority = Nothing } }	       
-
-getAuth :: Monad m => Request -> m URIAuthority
-getAuth r = case parseURIAuthority auth of
-			 Just x -> return x 
-			 Nothing -> fail $ "Error parsing URI authority '"
-				           ++ auth ++ "'"
-		 where auth = case findHeader HdrHost r of
-			      Just h -> h
-			      Nothing -> uriToAuthorityString (rqURI r)
-
-sendHTTP :: Stream s => s -> Request -> IO (Result Response)
-sendHTTP conn rq = 
-    do { let a_rq = fixHostHeader rq
-       ; rsp <- Exception.catch (main a_rq)
-                      (\e -> do { close conn; throw e })
-       ; let fn list = when (or $ map findConnClose list)
-                            (close conn)
-       ; either (\_ -> fn [rqHeaders rq])
-                (\r -> fn [rqHeaders rq,rspHeaders r])
-                rsp
-       ; return rsp
-       }
-    where       
--- From RFC 2616, section 8.2.3:
--- 'Because of the presence of older implementations, the protocol allows
--- ambiguous situations in which a client may send "Expect: 100-
--- continue" without receiving either a 417 (Expectation Failed) status
--- or a 100 (Continue) status. Therefore, when a client sends this
--- header field to an origin server (possibly via a proxy) from which it
--- has never seen a 100 (Continue) status, the client SHOULD NOT wait
--- for an indefinite period before sending the request body.'
---
--- Since we would wait forever, I have disabled use of 100-continue for now.
-        main :: Request -> IO (Result Response)
-        main rqst =
-            do 
-	       --let str = if null (rqBody rqst)
-               --              then show rqst
-               --              else show (insertHeader HdrExpect "100-continue" rqst)
-               writeBlock conn (show rqst)
-	       -- write body immediately, don't wait for 100 CONTINUE
-	       writeBlock conn (rqBody rqst)
-               rsp <- getResponseHead               
-               switchResponse True False rsp rqst
-        
-        -- reads and parses headers
-        getResponseHead :: IO (Result ResponseData)
-        getResponseHead =
-            do { lor <- readTillEmpty1 conn
-               ; return $ lor `bindE` parseResponseHead
-               }
-
-        -- Hmmm, this could go bad if we keep getting "100 Continue"
-        -- responses...  Except this should never happen according
-        -- to the RFC.
-        switchResponse :: Bool {- allow retry? -}
-                       -> Bool {- is body sent? -}
-                       -> Result ResponseData
-                       -> Request
-                       -> IO (Result Response)
-            
-        switchResponse _ _ (Left e) _ = return (Left e)
-                -- retry on connreset?
-                -- if we attempt to use the same socket then there is an excellent
-                -- chance that the socket is not in a completely closed state.
-
-        switchResponse allow_retry bdy_sent (Right (cd,rn,hdrs)) rqst =
-            case matchResponse (rqMethod rqst) cd of
-                Continue
-                    | not bdy_sent -> {- Time to send the body -}
-                        do { val <- writeBlock conn (rqBody rqst)
-                           ; case val of
-                                Left e -> return (Left e)
-                                Right _ ->
-                                    do { rsp <- getResponseHead
-                                       ; switchResponse allow_retry True rsp rqst
-                                       }
-                           }
-                    | otherwise -> {- keep waiting -}
-                        do { rsp <- getResponseHead
-                           ; switchResponse allow_retry bdy_sent rsp rqst                           
-                           }
-
-                Retry -> {- Request with "Expect" header failed.
-                                Trouble is the request contains Expects
-                                other than "100-Continue" -}
-                    do { writeBlock conn (show rqst ++ rqBody rqst)
-                       ; rsp <- getResponseHead
-                       ; switchResponse False bdy_sent rsp rqst
-                       }   
-                     
-                Done ->
-                    return (Right $ Response cd rn hdrs "")
-
-                DieHorribly str ->
-                    return $ Left $ ErrorParse ("Invalid response: " ++ str)
-
-                ExpectEntity ->
-                    let tc = lookupHeader HdrTransferEncoding hdrs
-                        cl = lookupHeader HdrContentLength hdrs
-                    in
-                    do { rslt <- case tc of
-                          Nothing -> 
-                              case cl of
-                                  Just x  -> linearTransfer conn (read x :: Int)
-                                  Nothing -> hopefulTransfer conn ""
-                          Just x  -> 
-                              case map toLower (trim x) of
-                                  "chunked" -> chunkedTransfer conn
-                                  _         -> uglyDeathTransfer conn
-                       ; return $ rslt `bindE` \(ftrs,bdy) -> Right (Response cd rn (hdrs++ftrs) bdy) 
-                       }
-
-        
-        -- Adds a Host header if one is NOT ALREADY PRESENT
-        fixHostHeader :: Request -> Request
-        fixHostHeader rq =
-            let uri = rqURI rq
-                host = uriToAuthorityString uri
-            in insertHeaderIfMissing HdrHost host rq
-                                     
-        -- Looks for a "Connection" header with the value "close".
-        -- Returns True when this is found.
-        findConnClose :: [Header] -> Bool
-        findConnClose hdrs =
-            case lookupHeader HdrConnection hdrs of
-                Nothing -> False
-                Just x  -> map toLower (trim x) == "close"
-
--- This function duplicates old Network.URI.authority behaviour.
-uriToAuthorityString :: URI -> String
-uriToAuthorityString URI{uriAuthority=Nothing} = ""
-uriToAuthorityString URI{uriAuthority=Just ua} = uriUserInfo ua ++
-                                                 uriRegName ua ++
-                                                 uriPort ua
-
--- | Receive and parse a HTTP request from the given Stream. Should be used 
---   for server side interactions.
-receiveHTTP :: Stream s => s -> IO (Result Request)
-receiveHTTP conn = do rq <- getRequestHead
-		      processRequest rq	    
-    where
-        -- reads and parses headers
-        getRequestHead :: IO (Result RequestData)
-        getRequestHead =
-            do { lor <- readTillEmpty1 conn
-               ; return $ lor `bindE` parseRequestHead
-               }
-	
-        processRequest (Left e) = return $ Left e
-	processRequest (Right (rm,uri,hdrs)) = 
-	    do -- FIXME : Also handle 100-continue.
-               let tc = lookupHeader HdrTransferEncoding hdrs
-                   cl = lookupHeader HdrContentLength hdrs
-	       rslt <- case tc of
-                          Nothing ->
-                              case cl of
-                                  Just x  -> linearTransfer conn (read x :: Int)
-                                  Nothing -> return (Right ([], "")) -- hopefulTransfer ""
-                          Just x  ->
-                              case map toLower (trim x) of
-                                  "chunked" -> chunkedTransfer conn
-                                  _         -> uglyDeathTransfer conn
-               
-               return $ rslt `bindE` \(ftrs,bdy) -> Right (Request uri rm (hdrs++ftrs) bdy)
-
-
--- | Very simple function, send a HTTP response over the given stream. This 
---   could be improved on to use different transfer types.
-respondHTTP :: Stream s => s -> Response -> IO ()
-respondHTTP conn rsp = do writeBlock conn (show rsp)
-                          -- write body immediately, don't wait for 100 CONTINUE
-                          writeBlock conn (rspBody rsp)
-			  return ()
-
--- The following functions were in the where clause of sendHTTP, they have
--- been moved to global scope so other functions can access them.		       
-
--- | Used when we know exactly how many bytes to expect.
-linearTransfer :: Stream s => s -> Int -> IO (Result ([Header],String))
-linearTransfer conn n
-    = do info <- readBlock conn n
-         return $ info `bindE` \str -> Right ([],str)
-
--- | Used when nothing about data is known,
---   Unfortunately waiting for a socket closure
---   causes bad behaviour.  Here we just
---   take data once and give up the rest.
-hopefulTransfer :: Stream s => s -> String -> IO (Result ([Header],String))
-hopefulTransfer conn str
-    = readLine conn >>= 
-      either (\v -> return $ Left v)
-             (\more -> if null more 
-                         then return (Right ([],str)) 
-                         else hopefulTransfer conn (str++more))
--- | A necessary feature of HTTP\/1.1
---   Also the only transfer variety likely to
---   return any footers.
-chunkedTransfer :: Stream s => s -> IO (Result ([Header],String))
-chunkedTransfer conn
-    =  chunkedTransferC conn 0 >>= \v ->
-       return $ v `bindE` \(ftrs,count,info) ->
-                let myftrs = Header HdrContentLength (show count) : ftrs              
-                in Right (myftrs,info)
-
-chunkedTransferC :: Stream s => s -> Int -> IO (Result ([Header],Int,String))
-chunkedTransferC conn n
-    =  readLine conn >>= \v -> case v of
-                  Left e -> return (Left e)
-                  Right line ->
-                      let size = ( if null line || (head line) == '0'
-                                     then 0
-                                     else case readHex line of
-                                        (n,_):_ -> n
-                                        _       -> 0
-                                     )
-                      in if size == 0
-                           then do { rs <- readTillEmpty2 conn []
-                                   ; return $
-                                        rs `bindE` \strs ->
-                                        parseHeaders strs `bindE` \ftrs ->
-                                        Right (ftrs,n,"")
-                                   }
-                           else do { some <- readBlock conn size
-                                   ; readLine conn
-                                   ; more <- chunkedTransferC conn (n+size)
-                                   ; return $ 
-                                        some `bindE` \cdata ->
-                                        more `bindE` \(ftrs,m,mdata) -> 
-                                        Right (ftrs,m,cdata++mdata) 
-                                   }                   
-
--- | Maybe in the future we will have a sensible thing
---   to do here, at that time we might want to change
---   the name.
-uglyDeathTransfer :: Stream s => s -> IO (Result ([Header],String))
-uglyDeathTransfer conn
-    = return $ Left $ ErrorParse "Unknown Transfer-Encoding"
-
--- | Remove leading crlfs then call readTillEmpty2 (not required by RFC)
-readTillEmpty1 :: Stream s => s -> IO (Result [String])
-readTillEmpty1 conn =
-    do { line <- readLine conn
-       ; case line of
-           Left e -> return $ Left e
-           Right s ->
-               if s == crlf
-                 then readTillEmpty1 conn
-                 else readTillEmpty2 conn [s]
-       }
-
--- | Read lines until an empty line (CRLF),
---   also accepts a connection close as end of
---   input, which is not an HTTP\/1.1 compliant
---   thing to do - so probably indicates an
---   error condition.
-readTillEmpty2 :: Stream s => s -> [String] -> IO (Result [String])
-readTillEmpty2 conn list =
-    do { line <- readLine conn
-       ; case line of
-           Left e -> return $ Left e
-           Right s ->
-               if s == crlf || null s
-                 then return (Right $ reverse (s:list))
-                 else readTillEmpty2 conn (s:list)
-       }
-
-        
------------------------------------------------------------------
------------------- A little friendly funtionality ---------------
------------------------------------------------------------------
-
-
-{-
-    I had a quick look around but couldn't find any RFC about
-    the encoding of data on the query string.  I did find an
-    IETF memo, however, so this is how I justify the urlEncode
-    and urlDecode methods.
-
-    Doc name: draft-tiwari-appl-wxxx-forms-01.txt  (look on www.ietf.org)
-
-    Reserved chars:  ";", "/", "?", ":", "@", "&", "=", "+", ",", and "$" are reserved.
-    Unwise: "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
-    URI delims: "<" | ">" | "#" | "%" | <">
-    Unallowed ASCII: <US-ASCII coded characters 00-1F and 7F hexadecimal>
-                     <US-ASCII coded character 20 hexadecimal>
-    Also unallowed:  any non-us-ascii character
-
-    Escape method: char -> '%' a b  where a, b :: Hex digits
--}
-
-urlEncode, urlDecode :: String -> String
-
-urlDecode ('%':a:b:rest) = chr (16 * digitToInt a + digitToInt b)
-                         : urlDecode rest
-urlDecode (h:t) = h : urlDecode t
-urlDecode [] = []
-
-urlEncode (h:t) =
-    let str = if reserved (ord h) then escape h else [h]
-    in str ++ urlEncode t
-    where
-        reserved x
-            | x >= ord 'a' && x <= ord 'z' = False
-            | x >= ord 'A' && x <= ord 'Z' = False
-            | x >= ord '0' && x <= ord '9' = False
-            | x <= 0x20 || x >= 0x7F = True
-            | otherwise = x `elem` map ord [';','/','?',':','@','&'
-                                           ,'=','+',',','$','{','}'
-                                           ,'|','\\','^','[',']','`'
-                                           ,'<','>','#','%','"']
-        -- wouldn't it be nice if the compiler
-        -- optimised the above for us?
-
-        escape x = 
-            let y = ord x 
-            in [ '%', intToDigit ((y `div` 16) .&. 0xf), intToDigit (y .&. 0xf) ]
-
-urlEncode [] = []
-            
-
-
--- Encode form variables, useable in either the
--- query part of a URI, or the body of a POST request.
--- I have no source for this information except experience,
--- this sort of encoding worked fine in CGI programming.
-urlEncodeVars :: [(String,String)] -> String
-urlEncodeVars ((n,v):t) =
-    let (same,diff) = partition ((==n) . fst) t
-    in urlEncode n ++ '=' : foldl (\x y -> x ++ ',' : urlEncode y) (urlEncode $ v) (map snd same)
-       ++ urlEncodeRest diff
-       where urlEncodeRest [] = []
-             urlEncodeRest diff = '&' : urlEncodeVars diff
-urlEncodeVars [] = []
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- The 'Network.HTTP' module provides a simple interface for sending and
+-- receiving content over HTTP in Haskell. Here's how to fetch a document from
+-- a URL and return it as a String:
+--
+-- >
+-- >    simpleHTTP (getRequest "http://www.haskell.org/") >>= fmap (take 100) . getResponseBody
+-- >        -- fetch document and return it (as a 'String'.)
+--
+-- Other functions let you control the submission and transfer of HTTP
+-- 'Request's and 'Response's more carefully, letting you integrate the use
+-- of 'Network.HTTP' functionality into your application.
+--
+-- The module also exports the main types of the package, 'Request' and 'Response',
+-- along with 'Header' and functions for working with these.
+--
+-- The actual functionality is implemented by modules in the @Network.HTTP.*@
+-- namespace, letting you either use the default implementation here
+-- by importing @Network.HTTP@ or, for more specific uses, selectively
+-- import the modules in @Network.HTTP.*@. To wit, more than one kind of
+-- representation of the bulk data that flows across a HTTP connection is
+-- supported. (see "Network.HTTP.HandleStream".)
+--
+-- /NOTE:/ The 'Request' send actions will normalize the @Request@ prior to transmission.
+-- Normalization such as having the request path be in the expected form and, possibly,
+-- introduce a default @Host:@ header if one isn't already present.
+-- Normalization also takes the @"user:pass\@"@ portion out of the the URI,
+-- if it was supplied, and converts it into @Authorization: Basic$ header.
+-- If you do not
+-- want the requests tampered with, but sent as-is, please import and use the
+-- the "Network.HTTP.HandleStream" or "Network.HTTP.Stream" modules instead. They
+-- export the same functions, but leaves construction and any normalization of
+-- @Request@s to the user.
+--
+-- /NOTE:/ This package only supports HTTP; it does not support HTTPS.
+-- Attempts to use HTTPS result in an error.
+-----------------------------------------------------------------------------
+module Network.HTTP
+       ( module Network.HTTP.Base
+       , module Network.HTTP.Headers
+
+         {- the functionality that the implementation modules,
+            Network.HTTP.HandleStream and Network.HTTP.Stream,
+            exposes:
+         -}
+       , simpleHTTP      -- :: Request -> IO (Result Response)
+       , simpleHTTP_     -- :: Stream s => s -> Request -> IO (Result Response)
+       , sendHTTP        -- :: Stream s => s -> Request -> IO (Result Response)
+       , sendHTTP_notify -- :: Stream s => s -> Request -> IO () -> IO (Result Response)
+       , receiveHTTP     -- :: Stream s => s -> IO (Result Request)
+       , respondHTTP     -- :: Stream s => s -> Response -> IO ()
+
+       , module Network.TCP
+
+       , getRequest      -- :: String -> Request_String
+       , headRequest     -- :: String -> Request_String
+       , postRequest     -- :: String -> Request_String
+       , postRequestWithBody -- :: String -> String -> String -> Request_String
+
+       , getResponseBody -- :: Result (Request ty) -> IO ty
+       , getResponseCode -- :: Result (Request ty) -> IO ResponseCode
+       ) where
+
+-----------------------------------------------------------------
+------------------ Imports --------------------------------------
+-----------------------------------------------------------------
+
+import Network.HTTP.Headers
+import Network.HTTP.Base
+import qualified Network.HTTP.HandleStream as S
+-- old implementation: import Network.HTTP.Stream
+import Network.TCP
+import Network.Stream ( Result )
+import Network.URI    ( parseURI )
+
+import Data.Maybe ( fromMaybe )
+
+{-
+ Note: if you switch over/back to using Network.HTTP.Stream here, you'll
+ have to wrap the results from 'openStream' as Connections via 'hstreamToConnection'
+ prior to delegating to the Network.HTTP.Stream functions.
+-}
+
+-- | @simpleHTTP req@ transmits the 'Request' @req@ by opening a /direct/, non-persistent
+-- connection to the HTTP server that @req@ is destined for, followed by transmitting
+-- it and gathering up the response as a 'Result'. Prior to sending the request,
+-- it is normalized (via 'normalizeRequest'). If you have to mediate the request
+-- via an HTTP proxy, you will have to normalize the request yourself. Or switch to
+-- using 'Network.Browser' instead.
+--
+-- Examples:
+--
+-- > simpleHTTP (getRequest "http://hackage.haskell.org/")
+-- > simpleHTTP (getRequest "http://hackage.haskell.org:8012/")
+
+simpleHTTP :: (HStream ty) => Request ty -> IO (Result (Response ty))
+simpleHTTP r = do
+  auth <- getAuth r
+  failHTTPS (rqURI r)
+  c <- openStream (host auth) (fromMaybe 80 (port auth))
+  let norm_r = normalizeRequest defaultNormalizeRequestOptions{normDoClose=True} r
+  simpleHTTP_ c norm_r
+
+-- | Identical to 'simpleHTTP', but acting on an already opened stream.
+simpleHTTP_ :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))
+simpleHTTP_ s r = do
+  let norm_r = normalizeRequest defaultNormalizeRequestOptions{normDoClose=True} r
+  S.sendHTTP s norm_r
+
+-- | @sendHTTP hStream httpRequest@ transmits @httpRequest@ (after normalization) over
+-- @hStream@, but does not alter the status of the connection, nor request it to be
+-- closed upon receiving the response.
+sendHTTP :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))
+sendHTTP conn rq = do
+  let norm_r = normalizeRequest defaultNormalizeRequestOptions rq
+  S.sendHTTP conn norm_r
+
+-- | @sendHTTP_notify hStream httpRequest action@ behaves like 'sendHTTP', but
+-- lets you supply an IO @action@ to execute once the request has been successfully
+-- transmitted over the connection. Useful when you want to set up tracing of
+-- request transmission and its performance.
+sendHTTP_notify :: HStream ty
+                => HandleStream ty
+                -> Request ty
+                -> IO ()
+                -> IO (Result (Response ty))
+sendHTTP_notify conn rq onSendComplete = do
+  let norm_r = normalizeRequest defaultNormalizeRequestOptions rq
+  S.sendHTTP_notify conn norm_r onSendComplete
+
+-- | @receiveHTTP hStream@ reads a 'Request' from the 'HandleStream' @hStream@
+receiveHTTP :: HStream ty => HandleStream ty -> IO (Result (Request ty))
+receiveHTTP conn = S.receiveHTTP conn
+
+-- | @respondHTTP hStream httpResponse@ transmits an HTTP 'Response' over
+-- the 'HandleStream' @hStream@. It could be used to implement simple web
+-- server interactions, performing the dual role to 'sendHTTP'.
+respondHTTP :: HStream ty => HandleStream ty -> Response ty -> IO ()
+respondHTTP conn rsp = S.respondHTTP conn rsp
+
+
+-- | A convenience constructor for a GET 'Request'.
+--
+-- If the URL isn\'t syntactically valid, the function raises an error.
+getRequest
+    :: String             -- ^URL to fetch
+    -> Request_String     -- ^The constructed request
+getRequest urlString =
+  case parseURI urlString of
+    Nothing -> error ("getRequest: Not a valid URL - " ++ urlString)
+    Just u  -> mkRequest GET u
+
+-- | A convenience constructor for a HEAD 'Request'.
+--
+-- If the URL isn\'t syntactically valid, the function raises an error.
+headRequest
+    :: String             -- ^URL to fetch
+    -> Request_String     -- ^The constructed request
+headRequest urlString =
+  case parseURI urlString of
+    Nothing -> error ("headRequest: Not a valid URL - " ++ urlString)
+    Just u  -> mkRequest HEAD u
+
+-- | A convenience constructor for a POST 'Request'.
+--
+-- If the URL isn\'t syntactically valid, the function raises an error.
+postRequest
+    :: String                   -- ^URL to POST to
+    -> Request_String           -- ^The constructed request
+postRequest urlString =
+  case parseURI urlString of
+    Nothing -> error ("postRequest: Not a valid URL - " ++ urlString)
+    Just u  -> mkRequest POST u
+
+-- | A convenience constructor for a POST 'Request'.
+--
+-- It constructs a request and sets the body as well as
+-- the Content-Type and Content-Length headers. The contents of the body
+-- are forced to calculate the value for the Content-Length header.
+--
+-- If the URL isn\'t syntactically valid, the function raises an error.
+postRequestWithBody
+    :: String                      -- ^URL to POST to
+    -> String                      -- ^Content-Type of body
+    -> String                      -- ^The body of the request
+    -> Request_String              -- ^The constructed request
+postRequestWithBody urlString typ body =
+  case parseURI urlString of
+    Nothing -> error ("postRequestWithBody: Not a valid URL - " ++ urlString)
+    Just u  -> setRequestBody (mkRequest POST u) (typ, body)
+
+-- | @getResponseBody response@ takes the response of a HTTP requesting action and
+-- tries to extricate the body of the 'Response' @response@. If the request action
+-- returned an error, an IO exception is raised.
+getResponseBody :: Result (Response ty) -> IO ty
+getResponseBody (Left err) = fail (show err)
+getResponseBody (Right r)  = return (rspBody r)
+
+-- | @getResponseCode response@ takes the response of a HTTP requesting action and
+-- tries to extricate the status code of the 'Response' @response@. If the request action
+-- returned an error, an IO exception is raised.
+getResponseCode :: Result (Response ty) -> IO ResponseCode
+getResponseCode (Left err) = fail (show err)
+getResponseCode (Right r)  = return (rspCode r)
+
+
+--
+-- * TODO
+--     - request pipelining
+--     - https upgrade (includes full TLS, i.e. SSL, implementation)
+--         - use of Stream classes will pay off
+--         - consider C implementation of encryption\/decryption
+--     - comm timeouts
+--     - MIME & entity stuff (happening in separate module)
+--     - support \"*\" uri-request-string for OPTIONS request method
+--
+--
+-- * Header notes:
+--
+--     [@Host@]
+--                  Required by HTTP\/1.1, if not supplied as part
+--                  of a request a default Host value is extracted
+--                  from the request-uri.
+--
+--     [@Connection@]
+--                  If this header is present in any request or
+--                  response, and it's value is "close", then
+--                  the current request\/response is the last
+--                  to be allowed on that connection.
+--
+--     [@Expect@]
+--                  Should a request contain a body, an Expect
+--                  header will be added to the request.  The added
+--                  header has the value \"100-continue\".  After
+--                  a 417 \"Expectation Failed\" response the request
+--                  is attempted again without this added Expect
+--                  header.
+--
+--     [@TransferEncoding,ContentLength,...@]
+--                  if request is inconsistent with any of these
+--                  header values then you may not receive any response
+--                  or will generate an error response (probably 4xx).
+--
+--
+-- * Response code notes
+-- Some response codes induce special behaviour:
+--
+--   [@1xx@]   \"100 Continue\" will cause any unsent request body to be sent.
+--             \"101 Upgrade\" will be returned.
+--             Other 1xx responses are ignored.
+--
+--   [@417@]   The reason for this code is \"Expectation failed\", indicating
+--             that the server did not like the Expect \"100-continue\" header
+--             added to a request.  Receipt of 417 will induce another
+--             request attempt (without Expect header), unless no Expect header
+--             had been added (in which case 417 response is returned).
diff --git a/Network/HTTP/Auth.hs b/Network/HTTP/Auth.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/Auth.hs
@@ -0,0 +1,221 @@
+{-# LANGUAGE CPP #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.Auth
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- Representing HTTP Auth values in Haskell.
+-- Right now, it contains mostly functionality needed by 'Network.Browser'.
+--
+-----------------------------------------------------------------------------
+module Network.HTTP.Auth
+       ( Authority(..)
+       , Algorithm(..)
+       , Challenge(..)
+       , Qop(..)
+
+       , headerToChallenge -- :: URI -> Header -> Maybe Challenge
+       , withAuthority     -- :: Authority -> Request ty -> String
+       ) where
+
+import Network.URI
+import Network.HTTP.Base
+import Network.HTTP.Utils
+import Network.HTTP.Headers ( Header(..) )
+import qualified Network.HTTP.MD5Aux as MD5 (md5s, Str(Str))
+import qualified Network.HTTP.Base64 as Base64 (encode)
+import Text.ParserCombinators.Parsec
+   ( Parser, char, many, many1, satisfy, parse, spaces, sepBy1 )
+
+import Data.Char
+import Data.Maybe
+import Data.Word ( Word8 )
+
+-- | @Authority@ specifies the HTTP Authentication method to use for
+-- a given domain/realm; @Basic@ or @Digest@.
+data Authority
+ = AuthBasic { auRealm    :: String
+             , auUsername :: String
+             , auPassword :: String
+             , auSite     :: URI
+             }
+ | AuthDigest{ auRealm     :: String
+             , auUsername  :: String
+             , auPassword  :: String
+             , auNonce     :: String
+             , auAlgorithm :: Maybe Algorithm
+             , auDomain    :: [URI]
+             , auOpaque    :: Maybe String
+             , auQop       :: [Qop]
+             }
+
+
+data Challenge
+ = ChalBasic  { chRealm   :: String }
+ | ChalDigest { chRealm   :: String
+              , chDomain  :: [URI]
+              , chNonce   :: String
+              , chOpaque  :: Maybe String
+              , chStale   :: Bool
+              , chAlgorithm ::Maybe Algorithm
+              , chQop     :: [Qop]
+              }
+
+-- | @Algorithm@ controls the digest algorithm to, @MD5@ or @MD5Session@.
+data Algorithm = AlgMD5 | AlgMD5sess
+    deriving(Eq)
+
+instance Show Algorithm where
+    show AlgMD5 = "md5"
+    show AlgMD5sess = "md5-sess"
+
+-- |
+data Qop = QopAuth | QopAuthInt
+    deriving(Eq,Show)
+
+-- | @withAuthority auth req@ generates a credentials value from the @auth@ 'Authority',
+-- in the context of the given request.
+--
+-- If a client nonce was to be used then this function might need to be of type ... -> BrowserAction String
+withAuthority :: Authority -> Request ty -> String
+withAuthority a rq = case a of
+        AuthBasic{}  -> "Basic " ++ base64encode (auUsername a ++ ':' : auPassword a)
+        AuthDigest{} ->
+            "Digest " ++
+             concat [ "username="  ++ quo (auUsername a)
+                    , ",realm="    ++ quo (auRealm a)
+                    , ",nonce="    ++ quo (auNonce a)
+                    , ",uri="      ++ quo digesturi
+                    , ",response=" ++ quo rspdigest
+                       -- plus optional stuff:
+                    , fromMaybe "" (fmap (\ alg -> ",algorithm=" ++ quo (show alg)) (auAlgorithm a))
+                    , fromMaybe "" (fmap (\ o   -> ",opaque=" ++ quo o) (auOpaque a))
+                    , if null (auQop a) then "" else ",qop=auth"
+                    ]
+    where
+        quo s = '"':s ++ "\""
+
+        rspdigest = map toLower (kd (md5 a1) (noncevalue ++ ":" ++ md5 a2))
+
+        a1, a2 :: String
+        a1 = auUsername a ++ ":" ++ auRealm a ++ ":" ++ auPassword a
+
+        {-
+        If the "qop" directive's value is "auth" or is unspecified, then A2
+        is:
+           A2  = Method ":" digest-uri-value
+        If the "qop" value is "auth-int", then A2 is:
+           A2  = Method ":" digest-uri-value ":" H(entity-body)
+        -}
+        a2 = show (rqMethod rq) ++ ":" ++ digesturi
+
+        digesturi = show (rqURI rq)
+        noncevalue = auNonce a
+
+type Octet = Word8
+
+-- FIXME: these probably only work right for latin-1 strings
+stringToOctets :: String -> [Octet]
+stringToOctets = map (fromIntegral . fromEnum)
+
+base64encode :: String -> String
+base64encode = Base64.encode . stringToOctets
+
+md5 :: String -> String
+md5 = MD5.md5s . MD5.Str
+
+kd :: String -> String -> String
+kd a b = md5 (a ++ ":" ++ b)
+
+
+
+
+-- | @headerToChallenge base www_auth@ tries to convert the @WWW-Authenticate@ header
+-- @www_auth@  into a 'Challenge' value.
+headerToChallenge :: URI -> Header -> Maybe Challenge
+headerToChallenge baseURI (Header _ str) =
+    case parse challenge "" str of
+        Left{} -> Nothing
+        Right (name,props) -> case name of
+            "basic"  -> mkBasic props
+            "digest" -> mkDigest props
+            _        -> Nothing
+    where
+        challenge :: Parser (String,[(String,String)])
+        challenge =
+            do { nme <- word
+               ; spaces
+               ; pps <- cprops
+               ; return (map toLower nme,pps)
+               }
+
+        cprops = sepBy1 cprop comma
+
+        comma = do { spaces ; _ <- char ',' ; spaces }
+
+        cprop =
+            do { nm <- word
+               ; _ <- char '='
+               ; val <- quotedstring
+               ; return (map toLower nm,val)
+               }
+
+        mkBasic, mkDigest :: [(String,String)] -> Maybe Challenge
+
+        mkBasic params = fmap ChalBasic (lookup "realm" params)
+
+        mkDigest params =
+            -- with Maybe monad
+            do { r <- lookup "realm" params
+               ; n <- lookup "nonce" params
+               ; return $
+                    ChalDigest { chRealm  = r
+                               , chDomain = (annotateURIs
+                                            $ map parseURI
+                                            $ words
+                                            $ fromMaybe []
+                                            $ lookup "domain" params)
+                               , chNonce  = n
+                               , chOpaque = lookup "opaque" params
+                               , chStale  = "true" == (map toLower
+                                           $ fromMaybe "" (lookup "stale" params))
+                               , chAlgorithm= readAlgorithm (fromMaybe "MD5" $ lookup "algorithm" params)
+                               , chQop    = readQop (fromMaybe "" $ lookup "qop" params)
+                               }
+               }
+
+        annotateURIs :: [Maybe URI] -> [URI]
+#if MIN_VERSION_network(2,4,0)
+        annotateURIs = map (`relativeTo` baseURI) . catMaybes
+#else
+        annotateURIs = (map (\u -> fromMaybe u (u `relativeTo` baseURI))) . catMaybes
+#endif
+
+        -- Change These:
+        readQop :: String -> [Qop]
+        readQop = catMaybes . (map strToQop) . (splitBy ',')
+
+        strToQop qs = case map toLower (trim qs) of
+            "auth"     -> Just QopAuth
+            "auth-int" -> Just QopAuthInt
+            _          -> Nothing
+
+        readAlgorithm astr = case map toLower (trim astr) of
+            "md5"      -> Just AlgMD5
+            "md5-sess" -> Just AlgMD5sess
+            _          -> Nothing
+
+word, quotedstring :: Parser String
+quotedstring =
+    do { _ <- char '"'  -- "
+       ; str <- many (satisfy $ not . (=='"'))
+       ; _ <- char '"'
+       ; return str
+       }
+
+word = many1 (satisfy (\x -> isAlphaNum x || x=='_' || x=='.' || x=='-' || x==':'))
diff --git a/Network/HTTP/Base.hs b/Network/HTTP/Base.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/Base.hs
@@ -0,0 +1,1002 @@
+{-# LANGUAGE CPP, ScopedTypeVariables #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.Base
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- Definitions of @Request@ and @Response@ types along with functions
+-- for normalizing them. It is assumed to be an internal module; user
+-- code should, if possible, import @Network.HTTP@ to access the functionality
+-- that this module provides.
+--
+-- Additionally, the module exports internal functions for working with URLs,
+-- and for handling the processing of requests and responses coming back.
+--
+-----------------------------------------------------------------------------
+module Network.HTTP.Base
+       (
+          -- ** Constants
+         httpVersion                 -- :: String
+
+          -- ** HTTP
+       , Request(..)
+       , Response(..)
+       , RequestMethod(..)
+
+       , Request_String
+       , Response_String
+       , HTTPRequest
+       , HTTPResponse
+
+          -- ** URL Encoding
+       , urlEncode
+       , urlDecode
+       , urlEncodeVars
+
+          -- ** URI authority parsing
+       , URIAuthority(..)
+       , parseURIAuthority
+
+          -- internal
+       , uriToAuthorityString   -- :: URI     -> String
+       , uriAuthToString        -- :: URIAuth -> String
+       , uriAuthPort            -- :: Maybe URI -> URIAuth -> Int
+       , reqURIAuth             -- :: Request ty -> URIAuth
+
+       , parseResponseHead      -- :: [String] -> Result ResponseData
+       , parseRequestHead       -- :: [String] -> Result RequestData
+
+       , ResponseNextStep(..)
+       , matchResponse
+       , ResponseData
+       , ResponseCode
+       , RequestData
+
+       , NormalizeRequestOptions(..)
+       , defaultNormalizeRequestOptions -- :: NormalizeRequestOptions ty
+       , RequestNormalizer
+
+       , normalizeRequest   -- :: NormalizeRequestOptions ty -> Request ty -> Request ty
+
+       , splitRequestURI
+
+       , getAuth
+       , normalizeRequestURI
+       , normalizeHostHeader
+       , findConnClose
+
+         -- internal export (for the use by Network.HTTP.{Stream,ByteStream} )
+       , linearTransfer
+       , hopefulTransfer
+       , chunkedTransfer
+       , uglyDeathTransfer
+       , readTillEmpty1
+       , readTillEmpty2
+
+       , defaultGETRequest
+       , defaultGETRequest_
+       , mkRequest
+       , setRequestBody
+
+       , defaultUserAgent
+       , httpPackageVersion
+       , libUA  {- backwards compatibility, will disappear..soon -}
+
+       , catchIO
+       , catchIO_
+       , responseParseError
+
+       , getRequestVersion
+       , getResponseVersion
+       , setRequestVersion
+       , setResponseVersion
+
+       , failHTTPS
+
+       ) where
+
+import Network.URI
+   ( URI(uriAuthority, uriPath, uriScheme)
+   , URIAuth(URIAuth, uriUserInfo, uriRegName, uriPort)
+   , parseURIReference
+   )
+
+import Control.Monad ( guard )
+
+import Data.Bits     ( (.&.), (.|.), shiftL, shiftR )
+import Data.Word     ( Word8 )
+import Data.Char     ( digitToInt, intToDigit, toLower, isDigit,
+                       isAscii, isAlphaNum, ord, chr )
+import Data.List     ( partition, find )
+import Data.Maybe    ( listToMaybe, fromMaybe )
+import Numeric       ( readHex )
+
+import Network.Stream
+import Network.BufferType ( BufferOp(..), BufferType(..) )
+import Network.HTTP.Headers
+import Network.HTTP.Utils ( trim, crlf, sp, readsOne )
+import qualified Network.HTTP.Base64 as Base64 (encode)
+
+import Text.Read.Lex (readDecP)
+import Text.ParserCombinators.ReadP
+   ( ReadP, readP_to_S, char, (<++), look, munch, munch1 )
+
+import Control.Exception as Exception (catch, IOException)
+
+import qualified Paths_HTTP as Self (version)
+import Data.Version (showVersion)
+
+-----------------------------------------------------------------
+------------------ URI Authority parsing ------------------------
+-----------------------------------------------------------------
+
+data URIAuthority = URIAuthority { user :: Maybe String,
+                                   password :: Maybe String,
+                                   host :: String,
+                                   port :: Maybe Int
+                                 } deriving (Eq,Show)
+
+-- | Parse the authority part of a URL.
+--
+-- > RFC 1732, section 3.1:
+-- >
+-- >       //<user>:<password>@<host>:<port>/<url-path>
+-- >  Some or all of the parts "<user>:<password>@", ":<password>",
+-- >  ":<port>", and "/<url-path>" may be excluded.
+parseURIAuthority :: String -> Maybe URIAuthority
+parseURIAuthority s = listToMaybe (map fst (readP_to_S pURIAuthority s))
+
+
+pURIAuthority :: ReadP URIAuthority
+pURIAuthority = do
+                (u,pw) <- (pUserInfo `before` char '@')
+                          <++ return (Nothing, Nothing)
+                h <- rfc2732host <++ munch (/=':')
+                p <- orNothing (char ':' >> readDecP)
+                look >>= guard . null
+                return URIAuthority{ user=u, password=pw, host=h, port=p }
+
+-- RFC2732 adds support for '[literal-ipv6-address]' in the host part of a URL
+rfc2732host :: ReadP String
+rfc2732host = do
+    _ <- char '['
+    res <- munch1 (/=']')
+    _ <- char ']'
+    return res
+
+pUserInfo :: ReadP (Maybe String, Maybe String)
+pUserInfo = do
+            u <- orNothing (munch (`notElem` ":@"))
+            p <- orNothing (char ':' >> munch (/='@'))
+            return (u,p)
+
+before :: Monad m => m a -> m b -> m a
+before a b = a >>= \x -> b >> return x
+
+orNothing :: ReadP a -> ReadP (Maybe a)
+orNothing p = fmap Just p <++ return Nothing
+
+-- This function duplicates old Network.URI.authority behaviour.
+uriToAuthorityString :: URI -> String
+uriToAuthorityString u = maybe "" uriAuthToString (uriAuthority u)
+
+uriAuthToString :: URIAuth -> String
+uriAuthToString ua =
+  concat [ uriUserInfo ua
+         , uriRegName ua
+         , uriPort ua
+         ]
+
+uriAuthPort :: Maybe URI -> URIAuth -> Int
+uriAuthPort mbURI u =
+  case uriPort u of
+    (':':s) -> readsOne id (default_port mbURI) s
+    _       -> default_port mbURI
+ where
+  default_port Nothing = default_http
+  default_port (Just url) =
+    case map toLower $ uriScheme url of
+      "http:" -> default_http
+      "https:" -> default_https
+        -- todo: refine
+      _ -> default_http
+
+  default_http  = 80
+  default_https = 443
+
+#if MIN_VERSION_base(4,13,0)
+failHTTPS :: MonadFail m => URI -> m ()
+#else
+failHTTPS :: Monad m => URI -> m ()
+#endif
+failHTTPS uri
+  | map toLower (uriScheme uri) == "https:" = fail "https not supported"
+  | otherwise = return ()
+
+-- Fish out the authority from a possibly normalized Request, i.e.,
+-- the information may either be in the request's URI or inside
+-- the Host: header.
+reqURIAuth :: Request ty -> URIAuth
+reqURIAuth req =
+  case uriAuthority (rqURI req) of
+    Just ua -> ua
+    _ -> case lookupHeader HdrHost (rqHeaders req) of
+           Nothing -> error ("reqURIAuth: no URI authority for: " ++ show req)
+           Just h  ->
+              case toHostPort h of
+                (ht,p) -> URIAuth { uriUserInfo = ""
+                                  , uriRegName  = ht
+                                  , uriPort     = p
+                                  }
+  where
+    -- Note: just in case you're wondering..the convention is to include the ':'
+    -- in the port part..
+   toHostPort h = break (==':') h
+
+-----------------------------------------------------------------
+------------------ HTTP Messages --------------------------------
+-----------------------------------------------------------------
+
+
+-- Protocol version
+httpVersion :: String
+httpVersion = "HTTP/1.1"
+
+
+-- | The HTTP request method, to be used in the 'Request' object.
+-- We are missing a few of the stranger methods, but these are
+-- not really necessary until we add full TLS.
+data RequestMethod = HEAD | PUT | GET | POST | DELETE | OPTIONS | TRACE | CONNECT | Custom String
+    deriving(Eq)
+
+instance Show RequestMethod where
+  show x =
+    case x of
+      HEAD     -> "HEAD"
+      PUT      -> "PUT"
+      GET      -> "GET"
+      POST     -> "POST"
+      DELETE   -> "DELETE"
+      OPTIONS  -> "OPTIONS"
+      TRACE    -> "TRACE"
+      CONNECT  -> "CONNECT"
+      Custom c -> c
+
+rqMethodMap :: [(String, RequestMethod)]
+rqMethodMap = [("HEAD",    HEAD),
+               ("PUT",     PUT),
+               ("GET",     GET),
+               ("POST",    POST),
+               ("DELETE",  DELETE),
+               ("OPTIONS", OPTIONS),
+               ("TRACE",   TRACE),
+               ("CONNECT", CONNECT)]
+
+--
+-- for backwards-ish compatibility; suggest
+-- migrating to new Req/Resp by adding type param.
+--
+type Request_String  = Request String
+type Response_String = Response String
+
+-- Hmm..I really want to use these for the record
+-- type, but it will upset codebases wanting to
+-- migrate (and live with using pre-HTTPbis versions.)
+type HTTPRequest a  = Request  a
+type HTTPResponse a = Response a
+
+-- | An HTTP Request.
+-- The 'Show' instance of this type is used for message serialisation,
+-- which means no body data is output.
+data Request a =
+     Request { rqURI       :: URI   -- ^ might need changing in future
+                                    --  1) to support '*' uri in OPTIONS request
+                                    --  2) transparent support for both relative
+                                    --     & absolute uris, although this should
+                                    --     already work (leave scheme & host parts empty).
+             , rqMethod    :: RequestMethod
+             , rqHeaders   :: [Header]
+             , rqBody      :: a
+             }
+
+-- Notice that request body is not included,
+-- this show function is used to serialise
+-- a request for the transport link, we send
+-- the body separately where possible.
+instance Show (Request a) where
+    show req@(Request u m h _) =
+        show m ++ sp ++ alt_uri ++ sp ++ ver ++ crlf
+        ++ foldr (++) [] (map show (dropHttpVersion h)) ++ crlf
+        where
+            ver = fromMaybe httpVersion (getRequestVersion req)
+            alt_uri = show $ if null (uriPath u) || head (uriPath u) /= '/'
+                        then u { uriPath = '/' : uriPath u }
+                        else u
+
+instance HasHeaders (Request a) where
+    getHeaders = rqHeaders
+    setHeaders rq hdrs = rq { rqHeaders=hdrs }
+
+-- | For easy pattern matching, HTTP response codes @xyz@ are
+-- represented as @(x,y,z)@.
+type ResponseCode  = (Int,Int,Int)
+
+-- | @ResponseData@ contains the head of a response payload;
+-- HTTP response code, accompanying text description + header
+-- fields.
+type ResponseData  = (ResponseCode,String,[Header])
+
+-- | @RequestData@ contains the head of a HTTP request; method,
+-- its URL along with the auxiliary/supporting header data.
+type RequestData   = (RequestMethod,URI,[Header])
+
+-- | An HTTP Response.
+-- The 'Show' instance of this type is used for message serialisation,
+-- which means no body data is output, additionally the output will
+-- show an HTTP version of 1.1 instead of the actual version returned
+-- by a server.
+data Response a =
+    Response { rspCode     :: ResponseCode
+             , rspReason   :: String
+             , rspHeaders  :: [Header]
+             , rspBody     :: a
+             }
+
+-- This is an invalid representation of a received response,
+-- since we have made the assumption that all responses are HTTP/1.1
+instance Show (Response a) where
+    show rsp@(Response (a,b,c) reason headers _) =
+        ver ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf
+        ++ foldr (++) [] (map show (dropHttpVersion headers)) ++ crlf
+     where
+      ver = fromMaybe httpVersion (getResponseVersion rsp)
+
+instance HasHeaders (Response a) where
+    getHeaders = rspHeaders
+    setHeaders rsp hdrs = rsp { rspHeaders=hdrs }
+
+
+------------------------------------------------------------------
+------------------ Request Building ------------------------------
+------------------------------------------------------------------
+
+-- | Deprecated. Use 'defaultUserAgent'
+libUA :: String
+libUA = "hs-HTTP-4000.0.9"
+{-# DEPRECATED libUA "Use defaultUserAgent instead (but note the user agent name change)" #-}
+
+-- | A default user agent string. The string is @\"haskell-HTTP/$version\"@
+-- where @$version@ is the version of this HTTP package.
+--
+defaultUserAgent :: String
+defaultUserAgent = "haskell-HTTP/" ++ httpPackageVersion
+
+-- | The version of this HTTP package as a string, e.g. @\"4000.1.2\"@. This
+-- may be useful to include in a user agent string so that you can determine
+-- from server logs what version of this package HTTP clients are using.
+-- This can be useful for tracking down HTTP compatibility quirks.
+--
+httpPackageVersion :: String
+httpPackageVersion = showVersion Self.version
+
+defaultGETRequest :: URI -> Request_String
+defaultGETRequest uri = defaultGETRequest_ uri
+
+defaultGETRequest_ :: BufferType a => URI -> Request a
+defaultGETRequest_ uri = mkRequest GET uri
+
+-- | 'mkRequest method uri' constructs a well formed
+-- request for the given HTTP method and URI. It does not
+-- normalize the URI for the request _nor_ add the required
+-- Host: header. That is done either explicitly by the user
+-- or when requests are normalized prior to transmission.
+mkRequest :: BufferType ty => RequestMethod -> URI -> Request ty
+mkRequest meth uri = req
+ where
+  req =
+    Request { rqURI      = uri
+            , rqBody     = empty
+            , rqHeaders  = [ Header HdrContentLength "0"
+                           , Header HdrUserAgent     defaultUserAgent
+                           ]
+            , rqMethod   = meth
+            }
+
+  empty = buf_empty (toBufOps req)
+
+-- set rqBody, Content-Type and Content-Length headers.
+setRequestBody :: Request_String -> (String, String) -> Request_String
+setRequestBody req (typ, body) = req' { rqBody=body }
+  where
+    req' = replaceHeader HdrContentType typ .
+           replaceHeader HdrContentLength (show $ length body) $
+           req
+
+{-
+    -- stub out the user info.
+  updAuth = fmap (\ x -> x{uriUserInfo=""}) (uriAuthority uri)
+
+  withHost =
+    case uriToAuthorityString uri{uriAuthority=updAuth} of
+      "" -> id
+      h  -> ((Header HdrHost h):)
+
+  uri_req
+   | forProxy  = uri
+   | otherwise = snd (splitRequestURI uri)
+-}
+
+
+toBufOps :: BufferType a => Request a -> BufferOp a
+toBufOps _ = bufferOps
+
+-----------------------------------------------------------------
+------------------ Parsing --------------------------------------
+-----------------------------------------------------------------
+
+-- Parsing a request
+parseRequestHead :: [String] -> Result RequestData
+parseRequestHead         [] = Left ErrorClosed
+parseRequestHead (com:hdrs) = do
+  (version,rqm,uri) <- requestCommand com (words com)
+  hdrs'              <- parseHeaders hdrs
+  return (rqm,uri,withVer version hdrs')
+ where
+  withVer [] hs = hs
+  withVer (h:_) hs = withVersion h hs
+
+  requestCommand l _yes@(rqm:uri:version) =
+    case (parseURIReference uri, lookup rqm rqMethodMap) of
+     (Just u, Just r) -> return (version,r,u)
+     (Just u, Nothing) -> return (version,Custom rqm,u)
+     _                -> parse_err l
+  requestCommand l _
+   | null l    = failWith ErrorClosed
+   | otherwise = parse_err l
+
+  parse_err l = responseParseError "parseRequestHead"
+                   ("Request command line parse failure: " ++ l)
+
+-- Parsing a response
+parseResponseHead :: [String] -> Result ResponseData
+parseResponseHead []         = failWith ErrorClosed
+parseResponseHead (sts:hdrs) = do
+  (version,code,reason)  <- responseStatus sts (words sts)
+  hdrs'                  <- parseHeaders hdrs
+  return (code,reason, withVersion version hdrs')
+ where
+  responseStatus _l _yes@(version:code:reason) =
+    return (version,match code,concatMap (++" ") reason)
+  responseStatus l _no
+    | null l    = failWith ErrorClosed  -- an assumption
+    | otherwise = parse_err l
+
+  parse_err l =
+    responseParseError
+        "parseResponseHead"
+        ("Response status line parse failure: " ++ l)
+
+  match [a,b,c] = (digitToInt a,
+                   digitToInt b,
+                   digitToInt c)
+  match _ = (-1,-1,-1)  -- will create appropriate behaviour
+
+-- To avoid changing the @RequestData@ and @ResponseData@ types
+-- just for this (and the upstream backwards compat. woes that
+-- will result in), encode version info as a custom header.
+-- Used by 'parseResponseData' and 'parseRequestData'.
+--
+-- Note: the Request and Response types do not currently represent
+-- the version info explicitly in their record types. You have to use
+-- {get,set}{Request,Response}Version for that.
+withVersion :: String -> [Header] -> [Header]
+withVersion v hs
+ | v == httpVersion = hs  -- don't bother adding it if the default.
+ | otherwise        = (Header (HdrCustom "X-HTTP-Version") v) : hs
+
+-- | @getRequestVersion req@ returns the HTTP protocol version of
+-- the request @req@. If @Nothing@, the default 'httpVersion' can be assumed.
+getRequestVersion :: Request a -> Maybe String
+getRequestVersion r = getHttpVersion r
+
+-- | @setRequestVersion v req@ returns a new request, identical to
+-- @req@, but with its HTTP version set to @v@.
+setRequestVersion :: String -> Request a -> Request a
+setRequestVersion s r = setHttpVersion r s
+
+
+-- | @getResponseVersion rsp@ returns the HTTP protocol version of
+-- the response @rsp@. If @Nothing@, the default 'httpVersion' can be
+-- assumed.
+getResponseVersion :: Response a -> Maybe String
+getResponseVersion r = getHttpVersion r
+
+-- | @setResponseVersion v rsp@ returns a new response, identical to
+-- @rsp@, but with its HTTP version set to @v@.
+setResponseVersion :: String -> Response a -> Response a
+setResponseVersion s r = setHttpVersion r s
+
+-- internal functions for accessing HTTP-version info in
+-- requests and responses. Not exported as it exposes ho
+-- version info is represented internally.
+
+getHttpVersion :: HasHeaders a => a -> Maybe String
+getHttpVersion r =
+  fmap toVersion      $
+   find isHttpVersion $
+    getHeaders r
+ where
+  toVersion (Header _ x) = x
+
+setHttpVersion :: HasHeaders a => a -> String -> a
+setHttpVersion r v =
+  setHeaders r $
+   withVersion v  $
+    dropHttpVersion $
+     getHeaders r
+
+dropHttpVersion :: [Header] -> [Header]
+dropHttpVersion hs = filter (not.isHttpVersion) hs
+
+isHttpVersion :: Header -> Bool
+isHttpVersion (Header (HdrCustom "X-HTTP-Version") _) = True
+isHttpVersion _ = False
+
+
+
+-----------------------------------------------------------------
+------------------ HTTP Send / Recv ----------------------------------
+-----------------------------------------------------------------
+
+data ResponseNextStep
+ = Continue
+ | Retry
+ | Done
+ | ExpectEntity
+ | DieHorribly String
+
+matchResponse :: RequestMethod -> ResponseCode -> ResponseNextStep
+matchResponse rqst rsp =
+    case rsp of
+        (1,0,0) -> Continue
+        (1,0,1) -> Done        -- upgrade to TLS
+        (1,_,_) -> Continue    -- default
+        (2,0,4) -> Done
+        (2,0,5) -> Done
+        (2,_,_) -> ans
+        (3,0,4) -> Done
+        (3,0,5) -> Done
+        (3,_,_) -> ans
+        (4,1,7) -> Retry       -- Expectation failed
+        (4,_,_) -> ans
+        (5,_,_) -> ans
+        (a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised")
+    where
+        ans | rqst == HEAD = Done
+            | otherwise    = ExpectEntity
+
+
+
+-----------------------------------------------------------------
+------------------ A little friendly funtionality ---------------
+-----------------------------------------------------------------
+
+
+{-
+    I had a quick look around but couldn't find any RFC about
+    the encoding of data on the query string.  I did find an
+    IETF memo, however, so this is how I justify the urlEncode
+    and urlDecode methods.
+
+    Doc name: draft-tiwari-appl-wxxx-forms-01.txt  (look on www.ietf.org)
+
+    Reserved chars:  ";", "/", "?", ":", "@", "&", "=", "+", ",", and "$" are reserved.
+    Unwise: "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
+    URI delims: "<" | ">" | "#" | "%" | <">
+    Unallowed ASCII: <US-ASCII coded characters 00-1F and 7F hexadecimal>
+                     <US-ASCII coded character 20 hexadecimal>
+    Also unallowed:  any non-us-ascii character
+
+    Escape method: char -> '%' a b  where a, b :: Hex digits
+-}
+
+replacement_character :: Char
+replacement_character = '\xfffd'
+
+-- | Encode a single Haskell Char to a list of Word8 values, in UTF8 format.
+--
+-- Shamelessly stolen from utf-8string-0.3.7
+encodeChar :: Char -> [Word8]
+encodeChar = map fromIntegral . go . ord
+ where
+  go oc
+   | oc <= 0x7f       = [oc]
+
+   | oc <= 0x7ff      = [ 0xc0 + (oc `shiftR` 6)
+                        , 0x80 + oc .&. 0x3f
+                        ]
+
+   | oc <= 0xffff     = [ 0xe0 + (oc `shiftR` 12)
+                        , 0x80 + ((oc `shiftR` 6) .&. 0x3f)
+                        , 0x80 + oc .&. 0x3f
+                        ]
+   | otherwise        = [ 0xf0 + (oc `shiftR` 18)
+                        , 0x80 + ((oc `shiftR` 12) .&. 0x3f)
+                        , 0x80 + ((oc `shiftR` 6) .&. 0x3f)
+                        , 0x80 + oc .&. 0x3f
+                        ]
+
+-- | Decode a UTF8 string packed into a list of Word8 values, directly to String
+--
+-- Shamelessly stolen from utf-8string-0.3.7
+decode :: [Word8] -> String
+decode [    ] = ""
+decode (c:cs)
+  | c < 0x80  = chr (fromEnum c) : decode cs
+  | c < 0xc0  = replacement_character : decode cs
+  | c < 0xe0  = multi1
+  | c < 0xf0  = multi_byte 2 0xf  0x800
+  | c < 0xf8  = multi_byte 3 0x7  0x10000
+  | c < 0xfc  = multi_byte 4 0x3  0x200000
+  | c < 0xfe  = multi_byte 5 0x1  0x4000000
+  | otherwise = replacement_character : decode cs
+  where
+    multi1 = case cs of
+      c1 : ds | c1 .&. 0xc0 == 0x80 ->
+        let d = ((fromEnum c .&. 0x1f) `shiftL` 6) .|.  fromEnum (c1 .&. 0x3f)
+        in if d >= 0x000080 then toEnum d : decode ds
+                            else replacement_character : decode ds
+      _ -> replacement_character : decode cs
+
+    multi_byte :: Int -> Word8 -> Int -> [Char]
+    multi_byte i mask overlong = aux i cs (fromEnum (c .&. mask))
+      where
+        aux 0 rs acc
+          | overlong <= acc && acc <= 0x10ffff &&
+            (acc < 0xd800 || 0xdfff < acc)     &&
+            (acc < 0xfffe || 0xffff < acc)      = chr acc : decode rs
+          | otherwise = replacement_character : decode rs
+
+        aux n (r:rs) acc
+          | r .&. 0xc0 == 0x80 = aux (n-1) rs
+                               $ shiftL acc 6 .|. fromEnum (r .&. 0x3f)
+
+        aux _ rs     _ = replacement_character : decode rs
+
+
+-- This function is a bit funny because potentially the input String could contain some actual Unicode
+-- characters (though this shouldn't happen for most use cases), so we have to preserve those characters
+-- while simultaneously decoding any UTF-8 data
+urlDecode :: String -> String
+urlDecode = go []
+  where
+    go bs ('%':a:b:rest)           = go (fromIntegral (16 * digitToInt a + digitToInt b) : bs) rest
+    go bs (h:t) | fromEnum h < 256 = go (fromIntegral (fromEnum h) : bs) t -- Treat ASCII as just another byte of UTF-8
+    go [] []                       = []
+    go [] (h:t)                    = h : go [] t -- h >= 256, so can't be part of any UTF-8 byte sequence
+    go bs rest                     = decode (reverse bs) ++ go [] rest
+
+
+urlEncode :: String -> String
+urlEncode     [] = []
+urlEncode (ch:t)
+  | (isAscii ch && isAlphaNum ch) || ch `elem` "-_.~" = ch : urlEncode t
+  | not (isAscii ch) = foldr escape (urlEncode t) (encodeChar ch)
+  | otherwise = escape (fromIntegral (fromEnum ch)) (urlEncode t)
+    where
+     escape b rs = '%':showH (b `div` 16) (showH (b `mod` 16) rs)
+
+     showH :: Word8 -> String -> String
+     showH x xs
+       | x <= 9    = to (o_0 + x) : xs
+       | otherwise = to (o_A + (x-10)) : xs
+      where
+       to  = toEnum  .  fromIntegral
+       fro = fromIntegral . fromEnum
+
+       o_0 = fro '0'
+       o_A = fro 'A'
+
+-- Encode form variables, useable in either the
+-- query part of a URI, or the body of a POST request.
+-- I have no source for this information except experience,
+-- this sort of encoding worked fine in CGI programming.
+urlEncodeVars :: [(String,String)] -> String
+urlEncodeVars ((n,v):t) =
+    let (same,diff) = partition ((==n) . fst) t
+    in urlEncode n ++ '=' : foldl (\x y -> x ++ ',' : urlEncode y) (urlEncode $ v) (map snd same)
+       ++ urlEncodeRest diff
+       where urlEncodeRest [] = []
+             urlEncodeRest diff = '&' : urlEncodeVars diff
+urlEncodeVars [] = []
+
+-- | @getAuth req@ fishes out the authority portion of the URL in a request's @Host@
+-- header.
+#if MIN_VERSION_base(4,13,0)
+getAuth :: MonadFail m => Request ty -> m URIAuthority
+#else
+getAuth :: Monad m => Request ty -> m URIAuthority
+#endif
+getAuth r =
+   -- ToDo: verify that Network.URI functionality doesn't take care of this (now.)
+  case parseURIAuthority auth of
+    Just x -> return x
+    Nothing -> fail $ "Network.HTTP.Base.getAuth: Error parsing URI authority '" ++ auth ++ "'"
+ where
+  auth = maybe (uriToAuthorityString uri) id (findHeader HdrHost r)
+  uri  = rqURI r
+
+{-# DEPRECATED normalizeRequestURI "Please use Network.HTTP.Base.normalizeRequest instead" #-}
+normalizeRequestURI :: Bool{-do close-} -> {-URI-}String -> Request ty -> Request ty
+normalizeRequestURI doClose h r =
+  (if doClose then replaceHeader HdrConnection "close" else id) $
+  insertHeaderIfMissing HdrHost h $
+    r { rqURI = (rqURI r){ uriScheme = ""
+                         , uriAuthority = Nothing
+                         }}
+
+-- | @NormalizeRequestOptions@ brings together the various defaulting\/normalization options
+-- over 'Request's. Use 'defaultNormalizeRequestOptions' for the standard selection of option
+data NormalizeRequestOptions ty
+ = NormalizeRequestOptions
+     { normDoClose   :: Bool
+     , normForProxy  :: Bool
+     , normUserAgent :: Maybe String
+     , normCustoms   :: [RequestNormalizer ty]
+     }
+
+-- | @RequestNormalizer@ is the shape of a (pure) function that rewrites
+-- a request into some normalized form.
+type RequestNormalizer ty = NormalizeRequestOptions ty -> Request ty -> Request ty
+
+defaultNormalizeRequestOptions :: NormalizeRequestOptions ty
+defaultNormalizeRequestOptions = NormalizeRequestOptions
+     { normDoClose   = False
+     , normForProxy  = False
+     , normUserAgent = Just defaultUserAgent
+     , normCustoms   = []
+     }
+
+-- | @normalizeRequest opts req@ is the entry point to use to normalize your
+-- request prior to transmission (or other use.) Normalization is controlled
+-- via the @NormalizeRequestOptions@ record.
+normalizeRequest :: NormalizeRequestOptions ty
+                 -> Request ty
+                 -> Request ty
+normalizeRequest opts req = foldr (\ f -> f opts) req normalizers
+ where
+  --normalizers :: [RequestNormalizer ty]
+  normalizers =
+     ( normalizeHostURI
+     : normalizeBasicAuth
+     : normalizeConnectionClose
+     : normalizeUserAgent
+     : normCustoms opts
+     )
+
+-- | @normalizeUserAgent ua x req@ augments the request @req@ with
+-- a @User-Agent: ua@ header if @req@ doesn't already have a
+-- a @User-Agent:@ set.
+normalizeUserAgent :: RequestNormalizer ty
+normalizeUserAgent opts req =
+  case normUserAgent opts of
+    Nothing -> req
+    Just ua ->
+     case findHeader HdrUserAgent req of
+       Just u  | u /= defaultUserAgent -> req
+       _ -> replaceHeader HdrUserAgent ua req
+
+-- | @normalizeConnectionClose opts req@ sets the header @Connection: close@
+-- to indicate one-shot behavior iff @normDoClose@ is @True@. i.e., it then
+-- _replaces_ any an existing @Connection:@ header in @req@.
+normalizeConnectionClose :: RequestNormalizer ty
+normalizeConnectionClose opts req
+ | normDoClose opts = replaceHeader HdrConnection "close" req
+ | otherwise        = req
+
+-- | @normalizeBasicAuth opts req@ sets the header @Authorization: Basic...@
+-- if the "user:pass@" part is present in the "http://user:pass@host/path"
+-- of the URI. If Authorization header was present already it is not replaced.
+normalizeBasicAuth :: RequestNormalizer ty
+normalizeBasicAuth _ req =
+  case getAuth req of
+    Just uriauth ->
+      case (user uriauth, password uriauth) of
+        (Just u, Just p) ->
+          insertHeaderIfMissing HdrAuthorization astr req
+            where
+              astr = "Basic " ++ base64encode (u ++ ":" ++ p)
+              base64encode = Base64.encode . stringToOctets :: String -> String
+              stringToOctets = map (fromIntegral . fromEnum) :: String -> [Word8]
+        (_, _) -> req
+    Nothing ->req
+
+-- | @normalizeHostURI forProxy req@ rewrites your request to have it
+-- follow the expected formats by the receiving party (proxy or server.)
+--
+normalizeHostURI :: RequestNormalizer ty
+normalizeHostURI opts req =
+  case splitRequestURI uri of
+    ("",_uri_abs)
+      | forProxy ->
+         case findHeader HdrHost req of
+           Nothing -> req -- no host/authority in sight..not much we can do.
+           Just h  -> req{rqURI=uri{ uriAuthority=Just URIAuth{uriUserInfo="", uriRegName=hst, uriPort=pNum}
+                                   , uriScheme=if (null (uriScheme uri)) then "http" else uriScheme uri
+                                   }}
+            where
+              hst = case span (/='@') user_hst of
+                       (as,'@':bs) ->
+                          case span (/=':') as of
+                            (_,_:_) -> bs
+                            _ -> user_hst
+                       _ -> user_hst
+
+              (user_hst, pNum) =
+                 case span isDigit (reverse h) of
+                   (ds,':':bs) -> (reverse bs, ':':reverse ds)
+                   _ -> (h,"")
+      | otherwise ->
+         case findHeader HdrHost req of
+           Nothing -> req -- no host/authority in sight..not much we can do...complain?
+           Just{}  -> req
+    (h,uri_abs)
+      | forProxy  -> insertHeaderIfMissing HdrHost h req
+      | otherwise -> replaceHeader HdrHost h req{rqURI=uri_abs} -- Note: _not_ stubbing out user:pass
+ where
+   uri0     = rqURI req
+     -- stub out the user:pass
+   uri      = uri0{uriAuthority=fmap (\ x -> x{uriUserInfo=""}) (uriAuthority uri0)}
+
+   forProxy = normForProxy opts
+
+{- Comments re: above rewriting:
+    RFC 2616, section 5.1.2:
+     "The most common form of Request-URI is that used to identify a
+      resource on an origin server or gateway. In this case the absolute
+      path of the URI MUST be transmitted (see section 3.2.1, abs_path) as
+      the Request-URI, and the network location of the URI (authority) MUST
+      be transmitted in a Host header field."
+   We assume that this is the case, so we take the host name from
+   the Host header if there is one, otherwise from the request-URI.
+   Then we make the request-URI an abs_path and make sure that there
+   is a Host header.
+-}
+
+splitRequestURI :: URI -> ({-authority-}String, URI)
+splitRequestURI uri = (uriToAuthorityString uri, uri{uriScheme="", uriAuthority=Nothing})
+
+-- Adds a Host header if one is NOT ALREADY PRESENT..
+{-# DEPRECATED normalizeHostHeader "Please use Network.HTTP.Base.normalizeRequest instead" #-}
+normalizeHostHeader :: Request ty -> Request ty
+normalizeHostHeader rq =
+  insertHeaderIfMissing HdrHost
+                        (uriToAuthorityString $ rqURI rq)
+                        rq
+
+-- Looks for a "Connection" header with the value "close".
+-- Returns True when this is found.
+findConnClose :: [Header] -> Bool
+findConnClose hdrs =
+  maybe False
+        (\ x -> map toLower (trim x) == "close")
+        (lookupHeader HdrConnection hdrs)
+
+-- | Used when we know exactly how many bytes to expect.
+linearTransfer :: (Int -> IO (Result a)) -> Int -> IO (Result ([Header],a))
+linearTransfer readBlk n = fmapE (\str -> Right ([],str)) (readBlk n)
+
+-- | Used when nothing about data is known,
+--   Unfortunately waiting for a socket closure
+--   causes bad behaviour.  Here we just
+--   take data once and give up the rest.
+hopefulTransfer :: BufferOp a
+                -> IO (Result a)
+                -> [a]
+                -> IO (Result ([Header],a))
+hopefulTransfer bufOps readL strs
+    = readL >>=
+      either (\v -> return $ Left v)
+             (\more -> if (buf_isEmpty bufOps more)
+                         then return (Right ([], buf_concat bufOps $ reverse strs))
+                         else hopefulTransfer bufOps readL (more:strs))
+
+-- | A necessary feature of HTTP\/1.1
+--   Also the only transfer variety likely to
+--   return any footers.
+chunkedTransfer :: BufferOp a
+                -> IO (Result a)
+                -> (Int -> IO (Result a))
+                -> IO (Result ([Header], a))
+chunkedTransfer bufOps readL readBlk = chunkedTransferC bufOps readL readBlk [] 0
+
+chunkedTransferC :: BufferOp a
+                 -> IO (Result a)
+                 -> (Int -> IO (Result a))
+                 -> [a]
+                 -> Int
+                 -> IO (Result ([Header], a))
+chunkedTransferC bufOps readL readBlk acc n = do
+  v <- readL
+  case v of
+    Left e -> return (Left e)
+    Right line
+     | size == 0 ->
+         -- last chunk read; look for trailing headers..
+        fmapE (\ strs -> do
+                 ftrs <- parseHeaders (map (buf_toStr bufOps) strs)
+                  -- insert (computed) Content-Length header.
+                 let ftrs' = Header HdrContentLength (show n) : ftrs
+                 return (ftrs',buf_concat bufOps (reverse acc)))
+
+              (readTillEmpty2 bufOps readL [])
+
+     | otherwise -> do
+         some <- readBlk size
+         case some of
+           Left e -> return (Left e)
+           Right cdata -> do
+               _ <- readL -- CRLF is mandated after the chunk block; ToDo: check that the line is empty.?
+               chunkedTransferC bufOps readL readBlk (cdata:acc) (n+size)
+     where
+      size
+       | buf_isEmpty bufOps line = 0
+       | otherwise =
+         case readHex (buf_toStr bufOps line) of
+          (hx,_):_ -> hx
+          _        -> 0
+
+-- | Maybe in the future we will have a sensible thing
+--   to do here, at that time we might want to change
+--   the name.
+uglyDeathTransfer :: String -> IO (Result ([Header],a))
+uglyDeathTransfer loc = return (responseParseError loc "Unknown Transfer-Encoding")
+
+-- | Remove leading crlfs then call readTillEmpty2 (not required by RFC)
+readTillEmpty1 :: BufferOp a
+               -> IO (Result a)
+               -> IO (Result [a])
+readTillEmpty1 bufOps readL =
+  readL >>=
+    either (return . Left)
+           (\ s ->
+               if buf_isLineTerm bufOps s
+                then readTillEmpty1 bufOps readL
+                else readTillEmpty2 bufOps readL [s])
+
+-- | Read lines until an empty line (CRLF),
+--   also accepts a connection close as end of
+--   input, which is not an HTTP\/1.1 compliant
+--   thing to do - so probably indicates an
+--   error condition.
+readTillEmpty2 :: BufferOp a
+               -> IO (Result a)
+               -> [a]
+               -> IO (Result [a])
+readTillEmpty2 bufOps readL list =
+    readL >>=
+      either (return . Left)
+             (\ s ->
+                if buf_isLineTerm bufOps s || buf_isEmpty bufOps s
+                 then return (Right $ reverse (s:list))
+                 else readTillEmpty2 bufOps readL (s:list))
+
+--
+-- Misc
+--
+
+-- | @catchIO a h@ handles IO action exceptions throughout codebase; version-specific
+-- tweaks better go here.
+catchIO :: IO a -> (IOException -> IO a) -> IO a
+catchIO a h = Exception.catch a h
+
+catchIO_ :: IO a -> IO a -> IO a
+catchIO_ a h = Exception.catch a (\(_ :: IOException) -> h)
+
+responseParseError :: String -> String -> Result a
+responseParseError loc v = failWith (ErrorParse (loc ++ ' ':v))
diff --git a/Network/HTTP/Base64.hs b/Network/HTTP/Base64.hs
--- a/Network/HTTP/Base64.hs
+++ b/Network/HTTP/Base64.hs
@@ -8,19 +8,18 @@
 -- Stability   :  experimental
 -- Portability :  portable
 --
--- Base64 encoding and decoding functions provided by Warwick Gray. 
--- See <http://homepages.paradise.net.nz/warrickg/haskell/http/#base64> 
+-- Base64 encoding and decoding functions provided by Warwick Gray.
+-- See <http://homepages.paradise.net.nz/warrickg/haskell/http/#base64>
 -- and <http://www.faqs.org/rfcs/rfc2045.html>.
 --
 -----------------------------------------------------------------------------
 
-module Network.HTTP.Base64 (
-    encode,
-    decode,
-    chop72
-) where
-
-
+module Network.HTTP.Base64
+   ( encode
+   , decode
+   , chop72
+   , Octet
+   ) where
 
 {------------------------------------------------------------------------
 This is what RFC2045 had to say:
@@ -135,10 +134,8 @@
    delimiters within base64-encoded bodies within multipart entities
    because no hyphen characters are used in the base64 encoding.
 
-
 ----------------------------------------------------------------------------}
 
-
 {-
 
 The following properties should hold:
@@ -151,26 +148,22 @@
 MIME applications might be undesireable.
 
 
-
-But: The Haskell98 Char type is at least 16bits (and often 32), these implementations assume only 
-     8 significant bits, which is more than enough for US-ASCII.  
+But: The Haskell98 Char type is at least 16bits (and often 32), these implementations assume only
+     8 significant bits, which is more than enough for US-ASCII.
 -}
 
 
-
-
-import Data.Array
-import Data.Bits
-import Data.Int
-import Data.Char (chr,ord)
+import Data.Array (Array, array, (!))
+import Data.Bits (shiftL, shiftR, (.&.), (.|.))
+import Data.Char (chr, ord)
 import Data.Word (Word8)
 
 type Octet = Word8
 
 encodeArray :: Array Int Char
-encodeArray = array (0,64) 
-          [ (0,'A'),  (1,'B'),  (2,'C'),  (3,'D'),  (4,'E'),  (5,'F')                    
-          , (6,'G'),  (7,'H'),  (8,'I'),  (9,'J'),  (10,'K'), (11,'L')                    
+encodeArray = array (0,64)
+          [ (0,'A'),  (1,'B'),  (2,'C'),  (3,'D'),  (4,'E'),  (5,'F')
+          , (6,'G'),  (7,'H'),  (8,'I'),  (9,'J'),  (10,'K'), (11,'L')
           , (12,'M'), (13,'N'), (14,'O'), (15,'P'), (16,'Q'), (17,'R')
           , (18,'S'), (19,'T'), (20,'U'), (21,'V'), (22,'W'), (23,'X')
           , (24,'Y'), (25,'Z'), (26,'a'), (27,'b'), (28,'c'), (29,'d')
@@ -184,11 +177,11 @@
 
 -- Convert between 4 base64 (6bits ea) integers and 1 ordinary integer (32 bits)
 -- clearly the upmost/leftmost 8 bits of the answer are 0.
--- Hack Alert: In the last entry of the answer, the upper 8 bits encode 
+-- Hack Alert: In the last entry of the answer, the upper 8 bits encode
 -- the integer number of 6bit groups encoded in that integer, ie 1, 2, 3.
 -- 0 represents a 4 :(
 int4_char3 :: [Int] -> [Char]
-int4_char3 (a:b:c:d:t) = 
+int4_char3 (a:b:c:d:t) =
     let n = (a `shiftL` 18 .|. b `shiftL` 12 .|. c `shiftL` 6 .|. d)
     in (chr (n `shiftR` 16 .&. 0xff))
      : (chr (n `shiftR` 8 .&. 0xff))
@@ -199,22 +192,24 @@
     in [ (chr (n `shiftR` 16 .&. 0xff))
        , (chr (n `shiftR` 8 .&. 0xff)) ]
 
-int4_char3 [a,b] = 
+int4_char3 [a,b] =
     let n = (a `shiftL` 18 .|. b `shiftL` 12)
     in [ (chr (n `shiftR` 16 .&. 0xff)) ]
 
-int4_char3 [] = []     
+int4_char3 [_] = error "Network.HTTP.Base64.int4_char3: impossible number of Ints."
 
+int4_char3 [] = []
 
 
 
+
 -- Convert triplets of characters to
 -- 4 base64 integers.  The last entries
 -- in the list may not produce 4 integers,
 -- a trailing 2 character group gives 3 integers,
 -- while a trailing single character gives 2 integers.
 char3_int4 :: [Char] -> [Int]
-char3_int4 (a:b:c:t) 
+char3_int4 (a:b:c:t)
     = let n = (ord a `shiftL` 16 .|. ord b `shiftL` 8 .|. ord c)
       in (n `shiftR` 18 .&. 0x3f) : (n `shiftR` 12 .&. 0x3f) : (n `shiftR` 6  .&. 0x3f) : (n .&. 0x3f) : char3_int4 t
 
@@ -223,7 +218,7 @@
       in [ (n `shiftR` 18 .&. 0x3f)
          , (n `shiftR` 12 .&. 0x3f)
          , (n `shiftR` 6  .&. 0x3f) ]
-    
+
 char3_int4 [a]
     = let n = (ord a `shiftL` 16)
       in [(n `shiftR` 18 .&. 0x3f),(n `shiftR` 12 .&. 0x3f)]
@@ -245,9 +240,11 @@
 
 -- Pads a base64 code to a multiple of 4 characters, using the special
 -- '=' character.
+quadruplets :: [Char] -> [Char]
 quadruplets (a:b:c:d:t) = a:b:c:d:quadruplets t
 quadruplets [a,b,c]     = [a,b,c,'=']      -- 16bit tail unit
 quadruplets [a,b]       = [a,b,'=','=']    -- 8bit tail unit
+quadruplets [_]         = error "Network.HTTP.Base64.quadruplets: impossible number of characters."
 quadruplets []          = []               -- 24bit tail unit
 
 
@@ -255,6 +252,7 @@
 enc = quadruplets . map enc1
 
 
+dcd :: String -> [Int]
 dcd [] = []
 dcd (h:t)
     | h <= 'Z' && h >= 'A'  =  ord h - ord 'A'      : dcd t
diff --git a/Network/HTTP/Cookie.hs b/Network/HTTP/Cookie.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/Cookie.hs
@@ -0,0 +1,141 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.Cookie
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- This module provides the data types and functions for working with HTTP cookies.
+-- Right now, it contains mostly functionality needed by 'Network.Browser'.
+--
+-----------------------------------------------------------------------------
+module Network.HTTP.Cookie
+       ( Cookie(..)
+       , cookieMatch          -- :: (String,String) -> Cookie -> Bool
+
+          -- functions for translating cookies and headers.
+       , cookiesToHeader      -- :: [Cookie] -> Header
+       , processCookieHeaders -- :: String -> [Header] -> ([String], [Cookie])
+       ) where
+
+import Network.HTTP.Headers
+
+import Data.Char
+import Data.List
+import Data.Maybe
+
+import Text.ParserCombinators.Parsec
+   ( Parser, char, many, many1, satisfy, parse, option, try
+   , (<|>), sepBy1
+   )
+
+------------------------------------------------------------------
+----------------------- Cookie Stuff -----------------------------
+------------------------------------------------------------------
+
+-- | @Cookie@ is the Haskell representation of HTTP cookie values.
+-- See its relevant specs for authoritative details.
+data Cookie
+ = MkCookie
+    { ckDomain  :: String
+    , ckName    :: String
+    , ckValue   :: String
+    , ckPath    :: Maybe String
+    , ckComment :: Maybe String
+    , ckVersion :: Maybe String
+    }
+    deriving(Show,Read)
+
+instance Eq Cookie where
+    a == b  =  ckDomain a == ckDomain b
+            && ckName a == ckName b
+            && ckPath a == ckPath b
+
+-- | @cookieToHeaders ck@ serialises @Cookie@s to an HTTP request header.
+cookiesToHeader :: [Cookie] -> Header
+cookiesToHeader cs = Header HdrCookie (mkCookieHeaderValue cs)
+
+-- | Turn a list of cookies into a key=value pair list, separated by
+-- semicolons.
+mkCookieHeaderValue :: [Cookie] -> String
+mkCookieHeaderValue = intercalate "; " . map mkCookieHeaderValue1
+  where
+    mkCookieHeaderValue1 c = ckName c ++ "=" ++ ckValue c
+
+-- | @cookieMatch (domain,path) ck@ performs the standard cookie
+-- match wrt the given domain and path.
+cookieMatch :: (String, String) -> Cookie -> Bool
+cookieMatch (dom,path) ck =
+ ckDomain ck `isSuffixOf` dom &&
+ case ckPath ck of
+   Nothing -> True
+   Just p  -> p `isPrefixOf` path
+
+
+-- | @processCookieHeaders dom hdrs@
+processCookieHeaders :: String -> [Header] -> ([String], [Cookie])
+processCookieHeaders dom hdrs = foldr (headerToCookies dom) ([],[]) hdrs
+
+-- | @headerToCookies dom hdr acc@
+headerToCookies :: String -> Header -> ([String], [Cookie]) -> ([String], [Cookie])
+headerToCookies dom (Header HdrSetCookie val) (accErr, accCookie) =
+    case parse cookies "" val of
+        Left{}  -> (val:accErr, accCookie)
+        Right x -> (accErr, x ++ accCookie)
+  where
+   cookies :: Parser [Cookie]
+   cookies = sepBy1 cookie (char ',')
+
+   cookie :: Parser Cookie
+   cookie =
+       do name <- word
+          _    <- spaces_l
+          _    <- char '='
+          _    <- spaces_l
+          val1 <- cvalue
+          args <- cdetail
+          return $ mkCookie name val1 args
+
+   cvalue :: Parser String
+
+   spaces_l = many (satisfy isSpace)
+
+   cvalue = quotedstring <|> many1 (satisfy $ not . (==';')) <|> return ""
+
+   -- all keys in the result list MUST be in lower case
+   cdetail :: Parser [(String,String)]
+   cdetail = many $
+       try (do _  <- spaces_l
+               _  <- char ';'
+               _  <- spaces_l
+               s1 <- word
+               _  <- spaces_l
+               s2 <- option "" (char '=' >> spaces_l >> cvalue)
+               return (map toLower s1,s2)
+           )
+
+   mkCookie :: String -> String -> [(String,String)] -> Cookie
+   mkCookie nm cval more =
+          MkCookie { ckName    = nm
+                   , ckValue   = cval
+                   , ckDomain  = map toLower (fromMaybe dom (lookup "domain" more))
+                   , ckPath    = lookup "path" more
+                   , ckVersion = lookup "version" more
+                   , ckComment = lookup "comment" more
+                   }
+headerToCookies _ _ acc = acc
+
+
+
+
+word, quotedstring :: Parser String
+quotedstring =
+    do _   <- char '"'  -- "
+       str <- many (satisfy $ not . (=='"'))
+       _   <- char '"'
+       return str
+
+word = many1 (satisfy (\x -> isAlphaNum x || x=='_' || x=='.' || x=='-' || x==':'))
diff --git a/Network/HTTP/HandleStream.hs b/Network/HTTP/HandleStream.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/HandleStream.hs
@@ -0,0 +1,252 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.HandleStream
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- A 'HandleStream'-based version of "Network.HTTP" interface.
+--
+-- For more detailed information about what the individual exports do, please consult
+-- the documentation for "Network.HTTP". /Notice/ however that the functions here do
+-- not perform any kind of normalization prior to transmission (or receipt); you are
+-- responsible for doing any such yourself, or, if you prefer, just switch to using
+-- "Network.HTTP" function instead.
+--
+-----------------------------------------------------------------------------
+module Network.HTTP.HandleStream
+       ( simpleHTTP      -- :: Request ty -> IO (Result (Response ty))
+       , simpleHTTP_     -- :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))
+       , sendHTTP        -- :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))
+       , sendHTTP_notify -- :: HStream ty => HandleStream ty -> Request ty -> IO () -> IO (Result (Response ty))
+       , receiveHTTP     -- :: HStream ty => HandleStream ty -> IO (Result (Request ty))
+       , respondHTTP     -- :: HStream ty => HandleStream ty -> Response ty -> IO ()
+
+       , simpleHTTP_debug -- :: FilePath -> Request DebugString -> IO (Response DebugString)
+       ) where
+
+-----------------------------------------------------------------
+------------------ Imports --------------------------------------
+-----------------------------------------------------------------
+
+import Network.BufferType
+import Network.Stream ( fmapE, Result )
+import Network.StreamDebugger ( debugByteStream )
+import Network.TCP (HStream(..), HandleStream )
+
+import Network.HTTP.Base
+import Network.HTTP.Headers
+import Network.HTTP.Utils ( trim, readsOne )
+
+import Data.Char (toLower)
+import Data.Maybe (fromMaybe)
+import Control.Exception (onException)
+import Control.Monad (when)
+
+-----------------------------------------------------------------
+------------------ Misc -----------------------------------------
+-----------------------------------------------------------------
+
+-- | @simpleHTTP@ transmits a resource across a non-persistent connection.
+simpleHTTP :: HStream ty => Request ty -> IO (Result (Response ty))
+simpleHTTP r = do
+  auth <- getAuth r
+  failHTTPS (rqURI r)
+  c <- openStream (host auth) (fromMaybe 80 (port auth))
+  simpleHTTP_ c r
+
+-- | @simpleHTTP_debug debugFile req@ behaves like 'simpleHTTP', but logs
+-- the HTTP operation via the debug file @debugFile@.
+simpleHTTP_debug :: HStream ty => FilePath -> Request ty -> IO (Result (Response ty))
+simpleHTTP_debug httpLogFile r = do
+  auth <- getAuth r
+  failHTTPS (rqURI r)
+  c0   <- openStream (host auth) (fromMaybe 80 (port auth))
+  c    <- debugByteStream httpLogFile c0
+  simpleHTTP_ c r
+
+-- | Like 'simpleHTTP', but acting on an already opened stream.
+simpleHTTP_ :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))
+simpleHTTP_ s r = sendHTTP s r
+
+-- | @sendHTTP hStream httpRequest@ transmits @httpRequest@ over
+-- @hStream@, but does not alter the status of the connection, nor request it to be
+-- closed upon receiving the response.
+sendHTTP :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))
+sendHTTP conn rq = sendHTTP_notify conn rq (return ())
+
+-- | @sendHTTP_notify hStream httpRequest action@ behaves like 'sendHTTP', but
+-- lets you supply an IO @action@ to execute once the request has been successfully
+-- transmitted over the connection. Useful when you want to set up tracing of
+-- request transmission and its performance.
+sendHTTP_notify :: HStream ty
+                => HandleStream ty
+                -> Request ty
+                -> IO ()
+                -> IO (Result (Response ty))
+sendHTTP_notify conn rq onSendComplete = do
+  when providedClose $ (closeOnEnd conn True)
+  onException (sendMain conn rq onSendComplete)
+              (close conn)
+ where
+  providedClose = findConnClose (rqHeaders rq)
+
+-- From RFC 2616, section 8.2.3:
+-- 'Because of the presence of older implementations, the protocol allows
+-- ambiguous situations in which a client may send "Expect: 100-
+-- continue" without receiving either a 417 (Expectation Failed) status
+-- or a 100 (Continue) status. Therefore, when a client sends this
+-- header field to an origin server (possibly via a proxy) from which it
+-- has never seen a 100 (Continue) status, the client SHOULD NOT wait
+-- for an indefinite period before sending the request body.'
+--
+-- Since we would wait forever, I have disabled use of 100-continue for now.
+sendMain :: HStream ty
+         => HandleStream ty
+         -> Request ty
+         -> (IO ())
+         -> IO (Result (Response ty))
+sendMain conn rqst onSendComplete = do
+      --let str = if null (rqBody rqst)
+      --              then show rqst
+      --              else show (insertHeader HdrExpect "100-continue" rqst)
+  -- TODO review throwing away of result
+  _ <- writeBlock conn (buf_fromStr bufferOps $ show rqst)
+    -- write body immediately, don't wait for 100 CONTINUE
+  -- TODO review throwing away of result
+  _ <- writeBlock conn (rqBody rqst)
+  onSendComplete
+  rsp <- getResponseHead conn
+  switchResponse conn True False rsp rqst
+
+   -- Hmmm, this could go bad if we keep getting "100 Continue"
+   -- responses...  Except this should never happen according
+   -- to the RFC.
+
+switchResponse :: HStream ty
+               => HandleStream ty
+               -> Bool {- allow retry? -}
+               -> Bool {- is body sent? -}
+               -> Result ResponseData
+               -> Request ty
+               -> IO (Result (Response ty))
+switchResponse _ _ _ (Left e) _ = return (Left e)
+                -- retry on connreset?
+                -- if we attempt to use the same socket then there is an excellent
+                -- chance that the socket is not in a completely closed state.
+
+switchResponse conn allow_retry bdy_sent (Right (cd,rn,hdrs)) rqst =
+   case matchResponse (rqMethod rqst) cd of
+     Continue
+      | not bdy_sent -> do {- Time to send the body -}
+        writeBlock conn (rqBody rqst) >>= either (return . Left)
+           (\ _ -> do
+              rsp <- getResponseHead conn
+              switchResponse conn allow_retry True rsp rqst)
+      | otherwise    -> do {- keep waiting -}
+        rsp <- getResponseHead conn
+        switchResponse conn allow_retry bdy_sent rsp rqst
+
+     Retry -> do {- Request with "Expect" header failed.
+                    Trouble is the request contains Expects
+                    other than "100-Continue" -}
+        -- TODO review throwing away of result
+        _ <- writeBlock conn ((buf_append bufferOps)
+                                     (buf_fromStr bufferOps (show rqst))
+                                     (rqBody rqst))
+        rsp <- getResponseHead conn
+        switchResponse conn False bdy_sent rsp rqst
+
+     Done -> do
+       when (findConnClose hdrs)
+            (closeOnEnd conn True)
+       return (Right $ Response cd rn hdrs (buf_empty bufferOps))
+
+     DieHorribly str -> do
+       close conn
+       return (responseParseError "Invalid response:" str)
+     ExpectEntity -> do
+       r <- fmapE (\ (ftrs,bdy) -> Right (Response cd rn (hdrs++ftrs) bdy)) $
+             maybe (maybe (hopefulTransfer bo (readLine conn) [])
+                       (\ x ->
+                          readsOne (linearTransfer (readBlock conn))
+                                   (return$responseParseError "unrecognized content-length value" x)
+                                   x)
+                        cl)
+                   (ifChunked (chunkedTransfer bo (readLine conn) (readBlock conn))
+                              (uglyDeathTransfer "sendHTTP"))
+                   tc
+       case r of
+         Left{} -> do
+           close conn
+           return r
+         Right (Response _ _ hs _) -> do
+           when (findConnClose hs)
+                (closeOnEnd conn True)
+           return r
+
+      where
+       tc = lookupHeader HdrTransferEncoding hdrs
+       cl = lookupHeader HdrContentLength hdrs
+       bo = bufferOps
+
+-- reads and parses headers
+getResponseHead :: HStream ty => HandleStream ty -> IO (Result ResponseData)
+getResponseHead conn =
+   fmapE (\es -> parseResponseHead (map (buf_toStr bufferOps) es))
+         (readTillEmpty1 bufferOps (readLine conn))
+
+-- | @receiveHTTP hStream@ reads a 'Request' from the 'HandleStream' @hStream@
+receiveHTTP :: HStream bufTy => HandleStream bufTy -> IO (Result (Request bufTy))
+receiveHTTP conn = getRequestHead >>= either (return . Left) processRequest
+  where
+    -- reads and parses headers
+   getRequestHead :: IO (Result RequestData)
+   getRequestHead = do
+      fmapE (\es -> parseRequestHead (map (buf_toStr bufferOps) es))
+            (readTillEmpty1 bufferOps (readLine conn))
+
+   processRequest (rm,uri,hdrs) =
+      fmapE (\ (ftrs,bdy) -> Right (Request uri rm (hdrs++ftrs) bdy)) $
+             maybe
+              (maybe (return (Right ([], buf_empty bo))) -- hopefulTransfer ""
+                     (\ x -> readsOne (linearTransfer (readBlock conn))
+                                      (return$responseParseError "unrecognized Content-Length value" x)
+                                      x)
+
+                     cl)
+              (ifChunked (chunkedTransfer bo (readLine conn) (readBlock conn))
+                         (uglyDeathTransfer "receiveHTTP"))
+              tc
+    where
+     -- FIXME : Also handle 100-continue.
+     tc = lookupHeader HdrTransferEncoding hdrs
+     cl = lookupHeader HdrContentLength hdrs
+     bo = bufferOps
+
+-- | @respondHTTP hStream httpResponse@ transmits an HTTP 'Response' over
+-- the 'HandleStream' @hStream@. It could be used to implement simple web
+-- server interactions, performing the dual role to 'sendHTTP'.
+respondHTTP :: HStream ty => HandleStream ty -> Response ty -> IO ()
+respondHTTP conn rsp = do
+  -- TODO: review throwing away of result
+  _ <- writeBlock conn (buf_fromStr bufferOps $ show rsp)
+   -- write body immediately, don't wait for 100 CONTINUE
+  -- TODO: review throwing away of result
+  _ <- writeBlock conn (rspBody rsp)
+  return ()
+
+------------------------------------------------------------------------------
+
+headerName :: String -> String
+headerName x = map toLower (trim x)
+
+ifChunked :: a -> a -> String -> a
+ifChunked a b s =
+  case headerName s of
+    "chunked" -> a
+    _ -> b
+
diff --git a/Network/HTTP/Headers.hs b/Network/HTTP/Headers.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/Headers.hs
@@ -0,0 +1,461 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.Headers
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- This module provides the data types for representing HTTP headers, and
+-- operations for looking up header values and working with sequences of
+-- header values in 'Request's and 'Response's. To avoid having to provide
+-- separate set of operations for doing so, we introduce a type class 'HasHeaders'
+-- to facilitate writing such processing using overloading instead.
+--
+-----------------------------------------------------------------------------
+module Network.HTTP.Headers
+   ( HasHeaders(..)     -- type class
+
+   , Header(..)
+   , mkHeader           -- :: HeaderName -> String -> Header
+   , hdrName            -- :: Header     -> HeaderName
+   , hdrValue           -- :: Header     -> String
+
+   , HeaderName(..)
+
+   , insertHeader          -- :: HasHeaders a => HeaderName -> String -> a -> a
+   , insertHeaderIfMissing -- :: HasHeaders a => HeaderName -> String -> a -> a
+   , insertHeaders         -- :: HasHeaders a => [Header] -> a -> a
+   , retrieveHeaders       -- :: HasHeaders a => HeaderName -> a -> [Header]
+   , replaceHeader         -- :: HasHeaders a => HeaderName -> String -> a -> a
+   , findHeader            -- :: HasHeaders a => HeaderName -> a -> Maybe String
+   , lookupHeader          -- :: HeaderName -> [Header] -> Maybe String
+
+   , parseHeader           -- :: parseHeader :: String -> Result Header
+   , parseHeaders          -- :: [String] -> Result [Header]
+
+   , headerMap             -- :: [(String, HeaderName)]
+
+   , HeaderSetter
+   ) where
+
+import Data.Char (toLower)
+import Network.Stream (Result, failParse)
+import Network.HTTP.Utils ( trim, split, crlf )
+
+-- | The @Header@ data type pairs header names & values.
+data Header = Header HeaderName String
+
+hdrName :: Header -> HeaderName
+hdrName (Header h _) = h
+
+hdrValue :: Header -> String
+hdrValue (Header _ v) = v
+
+-- | Header constructor as a function, hiding above rep.
+mkHeader :: HeaderName -> String -> Header
+mkHeader = Header
+
+instance Show Header where
+    show (Header key value) = shows key (':':' ':value ++ crlf)
+
+-- | HTTP @HeaderName@ type, a Haskell data constructor for each
+-- specification-defined header, prefixed with @Hdr@ and CamelCased,
+-- (i.e., eliding the @-@ in the process.) Should you require using
+-- a custom header, there's the @HdrCustom@ constructor which takes
+-- a @String@ argument.
+--
+-- Encoding HTTP header names differently, as Strings perhaps, is an
+-- equally fine choice..no decidedly clear winner, but let's stick
+-- with data constructors here.
+--
+data HeaderName
+    -- Generic Headers --
+ = HdrCacheControl
+ | HdrConnection
+ | HdrDate
+ | HdrPragma
+ | HdrTransferEncoding
+ | HdrUpgrade
+ | HdrVia
+    -- Request Headers --
+ | HdrAccept
+ | HdrAcceptCharset
+ | HdrAcceptEncoding
+ | HdrAcceptLanguage
+ | HdrAuthorization
+ | HdrCookie
+ | HdrExpect
+ | HdrFrom
+ | HdrHost
+ | HdrIfModifiedSince
+ | HdrIfMatch
+ | HdrIfNoneMatch
+ | HdrIfRange
+ | HdrIfUnmodifiedSince
+ | HdrMaxForwards
+ | HdrProxyAuthorization
+ | HdrRange
+ | HdrReferer
+ | HdrUserAgent
+    -- Response Headers
+ | HdrAge
+ | HdrLocation
+ | HdrProxyAuthenticate
+ | HdrPublic
+ | HdrRetryAfter
+ | HdrServer
+ | HdrSetCookie
+ | HdrTE
+ | HdrTrailer
+ | HdrVary
+ | HdrWarning
+ | HdrWWWAuthenticate
+    -- Entity Headers
+ | HdrAllow
+ | HdrContentBase
+ | HdrContentEncoding
+ | HdrContentLanguage
+ | HdrContentLength
+ | HdrContentLocation
+ | HdrContentMD5
+ | HdrContentRange
+ | HdrContentType
+ | HdrETag
+ | HdrExpires
+ | HdrLastModified
+    -- | MIME entity headers (for sub-parts)
+ | HdrContentTransferEncoding
+    -- | Allows for unrecognised or experimental headers.
+ | HdrCustom String -- not in header map below.
+
+instance Eq HeaderName where
+    HdrCustom a                == HdrCustom b                = (fmap toLower a) == (fmap toLower b)
+    HdrCacheControl            == HdrCacheControl            = True
+    HdrCacheControl            == _                          = False
+    _                          == HdrCacheControl            = False
+    HdrConnection              == HdrConnection              = True
+    HdrConnection              == _                          = False
+    _                          == HdrConnection              = False
+    HdrDate                    == HdrDate                    = True
+    HdrDate                    == _                          = False
+    _                          == HdrDate                    = False
+    HdrPragma                  == HdrPragma                  = True
+    HdrPragma                  == _                          = False
+    _                          == HdrPragma                  = False
+    HdrTransferEncoding        == HdrTransferEncoding        = True
+    HdrTransferEncoding        == _                          = False
+    _                          == HdrTransferEncoding        = False
+    HdrUpgrade                 == HdrUpgrade                 = True
+    HdrUpgrade                 == _                          = False
+    _                          == HdrUpgrade                 = False
+    HdrVia                     == HdrVia                     = True
+    HdrVia                     == _                          = False
+    _                          == HdrVia                     = False
+    HdrAccept                  == HdrAccept                  = True
+    HdrAccept                  == _                          = False
+    _                          == HdrAccept                  = False
+    HdrAcceptCharset           == HdrAcceptCharset           = True
+    HdrAcceptCharset           == _                          = False
+    _                          == HdrAcceptCharset           = False
+    HdrAcceptEncoding          == HdrAcceptEncoding          = True
+    HdrAcceptEncoding          == _                          = False
+    _                          == HdrAcceptEncoding          = False
+    HdrAcceptLanguage          == HdrAcceptLanguage          = True
+    HdrAcceptLanguage          == _                          = False
+    _                          == HdrAcceptLanguage          = False
+    HdrAuthorization           == HdrAuthorization           = True
+    HdrAuthorization           == _                          = False
+    _                          == HdrAuthorization           = False
+    HdrCookie                  == HdrCookie                  = True
+    HdrCookie                  == _                          = False
+    _                          == HdrCookie                  = False
+    HdrExpect                  == HdrExpect                  = True
+    HdrExpect                  == _                          = False
+    _                          == HdrExpect                  = False
+    HdrFrom                    == HdrFrom                    = True
+    HdrFrom                    == _                          = False
+    _                          == HdrFrom                    = False
+    HdrHost                    == HdrHost                    = True
+    HdrHost                    == _                          = False
+    _                          == HdrHost                    = False
+    HdrIfModifiedSince         == HdrIfModifiedSince         = True
+    HdrIfModifiedSince         == _                          = False
+    _                          == HdrIfModifiedSince         = False
+    HdrIfMatch                 == HdrIfMatch                 = True
+    HdrIfMatch                 == _                          = False
+    _                          == HdrIfMatch                 = False
+    HdrIfNoneMatch             == HdrIfNoneMatch             = True
+    HdrIfNoneMatch             == _                          = False
+    _                          == HdrIfNoneMatch             = False
+    HdrIfRange                 == HdrIfRange                 = True
+    HdrIfRange                 == _                          = False
+    _                          == HdrIfRange                 = False
+    HdrIfUnmodifiedSince       == HdrIfUnmodifiedSince       = True
+    HdrIfUnmodifiedSince       == _                          = False
+    _                          == HdrIfUnmodifiedSince       = False
+    HdrMaxForwards             == HdrMaxForwards             = True
+    HdrMaxForwards             == _                          = False
+    _                          == HdrMaxForwards             = False
+    HdrProxyAuthorization      == HdrProxyAuthorization      = True
+    HdrProxyAuthorization      == _                          = False
+    _                          == HdrProxyAuthorization      = False
+    HdrRange                   == HdrRange                   = True
+    HdrRange                   == _                          = False
+    _                          == HdrRange                   = False
+    HdrReferer                 == HdrReferer                 = True
+    HdrReferer                 == _                          = False
+    _                          == HdrReferer                 = False
+    HdrUserAgent               == HdrUserAgent               = True
+    HdrUserAgent               == _                          = False
+    _                          == HdrUserAgent               = False
+    HdrAge                     == HdrAge                     = True
+    HdrAge                     == _                          = False
+    _                          == HdrAge                     = False
+    HdrLocation                == HdrLocation                = True
+    HdrLocation                == _                          = False
+    _                          == HdrLocation                = False
+    HdrProxyAuthenticate       == HdrProxyAuthenticate       = True
+    HdrProxyAuthenticate       == _                          = False
+    _                          == HdrProxyAuthenticate       = False
+    HdrPublic                  == HdrPublic                  = True
+    HdrPublic                  == _                          = False
+    _                          == HdrPublic                  = False
+    HdrRetryAfter              == HdrRetryAfter              = True
+    HdrRetryAfter              == _                          = False
+    _                          == HdrRetryAfter              = False
+    HdrServer                  == HdrServer                  = True
+    HdrServer                  == _                          = False
+    _                          == HdrServer                  = False
+    HdrSetCookie               == HdrSetCookie               = True
+    HdrSetCookie               == _                          = False
+    _                          == HdrSetCookie               = False
+    HdrTE                      == HdrTE                      = True
+    HdrTE                      == _                          = False
+    _                          == HdrTE                      = False
+    HdrTrailer                 == HdrTrailer                 = True
+    HdrTrailer                 == _                          = False
+    _                          == HdrTrailer                 = False
+    HdrVary                    == HdrVary                    = True
+    HdrVary                    == _                          = False
+    _                          == HdrVary                    = False
+    HdrWarning                 == HdrWarning                 = True
+    HdrWarning                 == _                          = False
+    _                          == HdrWarning                 = False
+    HdrWWWAuthenticate         == HdrWWWAuthenticate         = True
+    HdrWWWAuthenticate         == _                          = False
+    _                          == HdrWWWAuthenticate         = False
+    HdrAllow                   == HdrAllow                   = True
+    HdrAllow                   == _                          = False
+    _                          == HdrAllow                   = False
+    HdrContentBase             == HdrContentBase             = True
+    HdrContentBase             == _                          = False
+    _                          == HdrContentBase             = False
+    HdrContentEncoding         == HdrContentEncoding         = True
+    HdrContentEncoding         == _                          = False
+    _                          == HdrContentEncoding         = False
+    HdrContentLanguage         == HdrContentLanguage         = True
+    HdrContentLanguage         == _                          = False
+    _                          == HdrContentLanguage         = False
+    HdrContentLength           == HdrContentLength           = True
+    HdrContentLength           == _                          = False
+    _                          == HdrContentLength           = False
+    HdrContentLocation         == HdrContentLocation         = True
+    HdrContentLocation         == _                          = False
+    _                          == HdrContentLocation         = False
+    HdrContentMD5              == HdrContentMD5              = True
+    HdrContentMD5              == _                          = False
+    _                          == HdrContentMD5              = False
+    HdrContentRange            == HdrContentRange            = True
+    HdrContentRange            == _                          = False
+    _                          == HdrContentRange            = False
+    HdrContentType             == HdrContentType             = True
+    HdrContentType             == _                          = False
+    _                          == HdrContentType             = False
+    HdrETag                    == HdrETag                    = True
+    HdrETag                    == _                          = False
+    _                          == HdrETag                    = False
+    HdrExpires                 == HdrExpires                 = True
+    HdrExpires                 == _                          = False
+    _                          == HdrExpires                 = False
+    HdrLastModified            == HdrLastModified            = True
+    HdrLastModified            == _                          = False
+    _                          == HdrLastModified            = False
+    HdrContentTransferEncoding == HdrContentTransferEncoding = True
+    HdrContentTransferEncoding == _                          = False
+    _                          == HdrContentTransferEncoding = False
+
+-- | @headerMap@ is a straight assoc list for translating between header names
+-- and values.
+headerMap :: [ (String,HeaderName) ]
+headerMap =
+   [ p "Cache-Control"        HdrCacheControl
+   , p "Connection"           HdrConnection
+   , p "Date"                 HdrDate
+   , p "Pragma"               HdrPragma
+   , p "Transfer-Encoding"    HdrTransferEncoding
+   , p "Upgrade"              HdrUpgrade
+   , p "Via"                  HdrVia
+   , p "Accept"               HdrAccept
+   , p "Accept-Charset"       HdrAcceptCharset
+   , p "Accept-Encoding"      HdrAcceptEncoding
+   , p "Accept-Language"      HdrAcceptLanguage
+   , p "Authorization"        HdrAuthorization
+   , p "Cookie"               HdrCookie
+   , p "Expect"               HdrExpect
+   , p "From"                 HdrFrom
+   , p "Host"                 HdrHost
+   , p "If-Modified-Since"    HdrIfModifiedSince
+   , p "If-Match"             HdrIfMatch
+   , p "If-None-Match"        HdrIfNoneMatch
+   , p "If-Range"             HdrIfRange
+   , p "If-Unmodified-Since"  HdrIfUnmodifiedSince
+   , p "Max-Forwards"         HdrMaxForwards
+   , p "Proxy-Authorization"  HdrProxyAuthorization
+   , p "Range"                HdrRange
+   , p "Referer"              HdrReferer
+   , p "User-Agent"           HdrUserAgent
+   , p "Age"                  HdrAge
+   , p "Location"             HdrLocation
+   , p "Proxy-Authenticate"   HdrProxyAuthenticate
+   , p "Public"               HdrPublic
+   , p "Retry-After"          HdrRetryAfter
+   , p "Server"               HdrServer
+   , p "Set-Cookie"           HdrSetCookie
+   , p "TE"                   HdrTE
+   , p "Trailer"              HdrTrailer
+   , p "Vary"                 HdrVary
+   , p "Warning"              HdrWarning
+   , p "WWW-Authenticate"     HdrWWWAuthenticate
+   , p "Allow"                HdrAllow
+   , p "Content-Base"         HdrContentBase
+   , p "Content-Encoding"     HdrContentEncoding
+   , p "Content-Language"     HdrContentLanguage
+   , p "Content-Length"       HdrContentLength
+   , p "Content-Location"     HdrContentLocation
+   , p "Content-MD5"          HdrContentMD5
+   , p "Content-Range"        HdrContentRange
+   , p "Content-Type"         HdrContentType
+   , p "ETag"                 HdrETag
+   , p "Expires"              HdrExpires
+   , p "Last-Modified"        HdrLastModified
+   , p "Content-Transfer-Encoding" HdrContentTransferEncoding
+   ]
+ where
+  p a b = (a,b)
+
+instance Show HeaderName where
+    show (HdrCustom s) = s
+    show x = case filter ((==x).snd) headerMap of
+                [] -> error "headerMap incomplete"
+                (h:_) -> fst h
+
+-- | @HasHeaders@ is a type class for types containing HTTP headers, allowing
+-- you to write overloaded header manipulation functions
+-- for both 'Request' and 'Response' data types, for instance.
+class HasHeaders x where
+    getHeaders :: x -> [Header]
+    setHeaders :: x -> [Header] -> x
+
+-- Header manipulation functions
+
+type HeaderSetter a = HeaderName -> String -> a -> a
+
+-- | @insertHeader hdr val x@ inserts a header with the given header name
+-- and value. Does not check for existing headers with same name, allowing
+-- duplicates to be introduce (use 'replaceHeader' if you want to avoid this.)
+insertHeader :: HasHeaders a => HeaderSetter a
+insertHeader name value x = setHeaders x newHeaders
+    where
+        newHeaders = (Header name value) : getHeaders x
+
+-- | @insertHeaderIfMissing hdr val x@ adds the new header only if no previous
+-- header with name @hdr@ exists in @x@.
+insertHeaderIfMissing :: HasHeaders a => HeaderSetter a
+insertHeaderIfMissing name value x = setHeaders x (newHeaders $ getHeaders x)
+    where
+        newHeaders list@(h@(Header n _): rest)
+            | n == name  = list
+            | otherwise  = h : newHeaders rest
+        newHeaders [] = [Header name value]
+
+-- | @replaceHeader hdr val o@ replaces the header @hdr@ with the
+-- value @val@, dropping any existing
+replaceHeader :: HasHeaders a => HeaderSetter a
+replaceHeader name value h = setHeaders h newHeaders
+    where
+        newHeaders = Header name value : [ x | x@(Header n _) <- getHeaders h, name /= n ]
+
+-- | @insertHeaders hdrs x@ appends multiple headers to @x@'s existing
+-- set.
+insertHeaders :: HasHeaders a => [Header] -> a -> a
+insertHeaders hdrs x = setHeaders x (getHeaders x ++ hdrs)
+
+-- | @retrieveHeaders hdrNm x@ gets a list of headers with 'HeaderName' @hdrNm@.
+retrieveHeaders :: HasHeaders a => HeaderName -> a -> [Header]
+retrieveHeaders name x = filter matchname (getHeaders x)
+    where
+        matchname (Header n _) = n == name
+
+-- | @findHeader hdrNm x@ looks up @hdrNm@ in @x@, returning the first
+-- header that matches, if any.
+findHeader :: HasHeaders a => HeaderName -> a -> Maybe String
+findHeader n x = lookupHeader n (getHeaders x)
+
+-- | @lookupHeader hdr hdrs@ locates the first header matching @hdr@ in the
+-- list @hdrs@.
+lookupHeader :: HeaderName -> [Header] -> Maybe String
+lookupHeader _ [] = Nothing
+lookupHeader v (Header n s:t)
+  |  v == n   =  Just s
+  | otherwise =  lookupHeader v t
+
+-- | @parseHeader headerNameAndValueString@ tries to unscramble a
+-- @header: value@ pairing and returning it as a 'Header'.
+parseHeader :: String -> Result Header
+parseHeader str =
+    case split ':' str of
+        Nothing -> failParse ("Unable to parse header: " ++ str)
+        Just (k,v) -> return $ Header (fn k) (trim $ drop 1 v)
+    where
+        fn k = case map snd $ filter (match k . fst) headerMap of
+                 [] -> (HdrCustom k)
+                 (h:_) -> h
+
+        match :: String -> String -> Bool
+        match s1 s2 = map toLower s1 == map toLower s2
+
+-- | @parseHeaders hdrs@ takes a sequence of strings holding header
+-- information and parses them into a set of headers (preserving their
+-- order in the input argument.) Handles header values split up over
+-- multiple lines.
+parseHeaders :: [String] -> Result [Header]
+parseHeaders = catRslts [] .
+                 map (parseHeader . clean) .
+                     joinExtended ""
+   where
+        -- Joins consecutive lines where the second line
+        -- begins with ' ' or '\t'.
+        joinExtended old      [] = [old]
+        joinExtended old (h : t)
+          | isLineExtension h    = joinExtended (old ++ ' ' : tail h) t
+          | otherwise            = old : joinExtended h t
+
+        isLineExtension (x:_) = x == ' ' || x == '\t'
+        isLineExtension _ = False
+
+        clean [] = []
+        clean (h:t) | h `elem` "\t\r\n" = ' ' : clean t
+                    | otherwise = h : clean t
+
+        -- tolerant of errors?  should parse
+        -- errors here be reported or ignored?
+        -- currently ignored.
+        catRslts :: [a] -> [Result a] -> Result [a]
+        catRslts list (h:t) =
+            case h of
+                Left _ -> catRslts list t
+                Right v -> catRslts (v:list) t
+        catRslts list [] = Right $ reverse list
diff --git a/Network/HTTP/MD5.hs b/Network/HTTP/MD5.hs
deleted file mode 100644
--- a/Network/HTTP/MD5.hs
+++ /dev/null
@@ -1,44 +0,0 @@
------------------------------------------------------------------------------
--- |
--- Module      :  Data.Digest.MD5
--- Copyright   :  (c) Dominic Steinitz 2004
--- License     :  BSD-style (see the file ReadMe.tex)
--- 
--- Maintainer  :  dominic.steinitz@blueyonder.co.uk
--- Stability   :  experimental
--- Portability :  portable
---
--- Takes the MD5 module supplied by Ian Lynagh and wraps it so it
--- takes [Octet] and returns [Octet] where the length of the result
--- is always 16.
--- See <http://web.comlab.ox.ac.uk/oucl/work/ian.lynagh/>
--- and <http://www.ietf.org/rfc/rfc1321.txt>.
---
------------------------------------------------------------------------------
-
-module Network.HTTP.MD5 (
-   -- * Function Types
-   hash) where
-
-import Data.Char(chr)
-import Data.List(unfoldr)
-import Numeric(readHex)
-
-import Network.HTTP.MD5Aux
-import Data.Word (Word8)
-
-type Octet = Word8
-
--- | Take [Octet] and return [Octet] according to the standard.
---   The length of the result is always 16 octets or 128 bits as required
---   by the standard.
-
-hash :: [Octet] -> [Octet]
-hash xs = 
-   unfoldr f $ md5s $ Str $ map (chr . fromIntegral) xs
-      where f :: String -> Maybe (Octet,String)
-            f [] = 
-               Nothing
-            f (x:y:zs) = 
-               Just (fromIntegral a,zs)
-	       where [(a,_)] = readHex (x:y:[])
diff --git a/Network/HTTP/MD5Aux.hs b/Network/HTTP/MD5Aux.hs
--- a/Network/HTTP/MD5Aux.hs
+++ b/Network/HTTP/MD5Aux.hs
@@ -1,31 +1,15 @@
-module Network.HTTP.MD5Aux 
+module Network.HTTP.MD5Aux
    (md5,  md5s,  md5i,
-    MD5(..), ABCD(..), 
+    MD5(..), ABCD(..),
     Zord64, Str(..), BoolList(..), WordList(..)) where
 
-import Data.Char
-import Data.Bits
-import Data.Word
-
-{-
-Nasty kludge to create a type Zord64 which is really a Word64 but works
-how we want in hugs ands nhc98 too...
-Also need a rotate left function that actually works.
-
-#ifdef __GLASGOW_HASKELL__
-#define rotL rotateL
-#include "Zord64_EASY.hs"
-#else
-
-> import Zord64_HARD
- 
-> rotL :: Word32 -> Rotation -> Word32
-> rotL a s = shiftL a s .|. shiftL a (s-32)
-
-#endif
--}
+import Data.Char (ord, chr)
+import Data.Bits (rotateL, shiftL, shiftR, (.&.), (.|.), xor, complement)
+import Data.Word (Word32, Word64)
 
+rotL :: Word32 -> Int -> Word32
 rotL x = rotateL x
+
 type Zord64 = Word64
 
 -- ===================== TYPES AND CLASS DEFINTIONS ========================
@@ -36,7 +20,7 @@
 newtype ABCD = ABCD (Word32, Word32, Word32, Word32) deriving (Eq, Show)
 newtype Str = Str String
 newtype BoolList = BoolList [Bool]
-newtype WordList = WordList ([Word32], Zord64)
+newtype WordList = WordList ([Word32], Word64)
 
 -- Anything we want to work out the MD5 of must be an instance of class MD5
 
@@ -45,7 +29,7 @@
  --                     \      \   \------ the rest of the input
  --                      \      \--------- the number of bits returned
  --                       \--------------- the bits returned in 32bit words
- len_pad :: Zord64 -> a -> a         -- append the padding and length
+ len_pad :: Word64 -> a -> a         -- append the padding and length
  finished :: a -> Bool               -- Have we run out of input yet?
 
 
@@ -107,7 +91,11 @@
 instance Num ABCD where
  ABCD (a1, b1, c1, d1) + ABCD (a2, b2, c2, d2) = ABCD (a1 + a2, b1 + b2, c1 + c2, d1 + d2)
 
-
+ (-)         = error "(-){ABCD}: no instance method defined"
+ (*)         = error "(*){ABCD}: no instance method defined"
+ signum      = error "signum{ABCD}: no instance method defined"
+ fromInteger = error "fromInteger{ABCD}: no instance method defined"
+ abs         = error "abs{ABCD}: no instance method defined"
 -- ===================== EXPORTED FUNCTIONS ========================
 
 
@@ -118,7 +106,7 @@
 md5 m = md5_main False 0 magic_numbers m
 
 
--- Returns a hex number ala the md5sum program
+-- Returns a hex number à la md5sum program
 
 md5s :: (MD5 a) => a -> String
 md5s = abcd_to_string . md5
@@ -139,7 +127,7 @@
 
 md5_main :: (MD5 a) =>
             Bool   -- Have we added padding yet?
-         -> Zord64 -- The length so far mod 2^64
+         -> Word64 -- The length so far mod 2^64
          -> ABCD   -- The initial state
          -> a      -- The non-processed portion of the message
          -> ABCD   -- The resulting state
@@ -348,7 +336,7 @@
 -- Convert the size into a list of characters used by the len_pad function
 -- for strings
 
-length_to_chars :: Int -> Zord64 -> String
+length_to_chars :: Int -> Word64 -> String
 length_to_chars 0 _ = []
 length_to_chars p n = this:length_to_chars (p-1) (shiftR n 8)
          where this = chr $ fromIntegral $ n .&. 255
diff --git a/Network/HTTP/Proxy.hs b/Network/HTTP/Proxy.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/Proxy.hs
@@ -0,0 +1,226 @@
+{-# LANGUAGE CPP #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.Proxy
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- Handling proxy server settings and their resolution.
+--
+-----------------------------------------------------------------------------
+module Network.HTTP.Proxy
+       ( Proxy(..)
+       , noProxy     -- :: Proxy
+       , fetchProxy  -- :: Bool -> IO Proxy
+       , parseProxy  -- :: String -> Maybe Proxy
+       ) where
+
+{-
+#if !defined(WIN32) && defined(mingw32_HOST_OS)
+#define WIN32 1
+#endif
+-}
+
+import Control.Monad ( when, mplus, join, liftM2 )
+
+#if defined(WIN32)
+import Network.HTTP.Base ( catchIO )
+import Control.Monad ( liftM )
+import Data.List ( isPrefixOf )
+#endif
+import Network.HTTP.Utils ( dropWhileTail, chopAtDelim )
+import Network.HTTP.Auth
+import Network.URI
+   ( URI(..), URIAuth(..), parseAbsoluteURI, unEscapeString )
+import System.IO ( hPutStrLn, stderr )
+import System.Environment
+
+{-
+#if !defined(WIN32) && defined(mingw32_HOST_OS)
+#define WIN32 1
+#endif
+-}
+
+#if defined(WIN32)
+import System.Win32.Types   ( DWORD, HKEY )
+import System.Win32.Registry( hKEY_CURRENT_USER, regOpenKey, regCloseKey, regQueryValueEx )
+import Control.Exception    ( bracket )
+import Foreign              ( toBool, Storable(peek, sizeOf), castPtr, alloca )
+
+#if MIN_VERSION_Win32(2,8,0)
+import System.Win32.Registry( regQueryDefaultValue )
+#else
+import System.Win32.Registry( regQueryValue )
+#endif
+#endif
+
+-- | HTTP proxies (or not) are represented via 'Proxy', specifying if a
+-- proxy should be used for the request (see 'Network.Browser.setProxy')
+data Proxy
+ = NoProxy                 -- ^ Don't use a proxy.
+ | Proxy String
+         (Maybe Authority) -- ^ Use the proxy given. Should be of the
+                           -- form "http:\/\/host:port", "host", "host:port", or "http:\/\/host".
+                           -- Additionally, an optional 'Authority' for authentication with the proxy.
+
+
+noProxy :: Proxy
+noProxy = NoProxy
+
+-- | @envProxyString@ locates proxy server settings by looking
+-- up env variable @HTTP_PROXY@ (or its lower-case equivalent.)
+-- If no mapping found, returns @Nothing@.
+envProxyString :: IO (Maybe String)
+envProxyString = do
+  env <- getEnvironment
+  return (lookup "http_proxy" env `mplus` lookup "HTTP_PROXY" env)
+
+-- | @proxyString@ tries to locate the user's proxy server setting.
+-- Consults environment variable, and in case of Windows, by querying
+-- the Registry (cf. @registryProxyString@.)
+proxyString :: IO (Maybe String)
+proxyString = liftM2 mplus envProxyString windowsProxyString
+
+windowsProxyString :: IO (Maybe String)
+#if !defined(WIN32)
+windowsProxyString = return Nothing
+#else
+windowsProxyString = liftM (>>= parseWindowsProxy) registryProxyString
+
+registryProxyLoc :: (HKEY,String)
+registryProxyLoc = (hive, path)
+  where
+    -- some sources say proxy settings should be at
+    -- HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows
+    --                   \CurrentVersion\Internet Settings\ProxyServer
+    -- but if the user sets them with IE connection panel they seem to
+    -- end up in the following place:
+    hive  = hKEY_CURRENT_USER
+    path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"
+
+-- read proxy settings from the windows registry; this is just a best
+-- effort and may not work on all setups.
+registryProxyString :: IO (Maybe String)
+registryProxyString = catchIO
+  (bracket (uncurry regOpenKey registryProxyLoc) regCloseKey $ \hkey -> do
+    enable <- fmap toBool $ regQueryValueDWORD hkey "ProxyEnable"
+    if enable
+#if MIN_VERSION_Win32(2,8,0)
+        then fmap Just $ regQueryDefaultValue hkey "ProxyServer"
+#elif MIN_VERSION_Win32(2,6,0)
+        then fmap Just $ regQueryValue hkey "ProxyServer"
+#else
+        then fmap Just $ regQueryValue hkey (Just "ProxyServer")
+#endif
+        else return Nothing)
+  (\_ -> return Nothing)
+
+-- the proxy string is in the format "http=x.x.x.x:yyyy;https=...;ftp=...;socks=..."
+-- even though the following article indicates otherwise
+-- https://support.microsoft.com/en-us/kb/819961
+--
+-- to be sure, parse strings where each entry in the ';'-separated list above is
+-- either in the format "protocol=..." or "protocol://..."
+--
+-- only return the first "http" of them, if it exists
+parseWindowsProxy :: String -> Maybe String
+parseWindowsProxy s =
+  case proxies of
+    x:_ -> Just x
+    _   -> Nothing
+  where
+    parts = split ';' s
+    pr x = case break (== '=') x of
+      (p, []) -> p  -- might be in format http://
+      (p, u)  -> p ++ "://" ++ drop 1 u
+
+    proxies = filter (isPrefixOf "http://") . map pr $ parts
+
+    split :: Eq a => a -> [a] -> [[a]]
+    split _ [] = []
+    split a xs = case break (a ==) xs of
+      (ys, [])   -> [ys]
+      (ys, _:zs) -> ys:split a zs
+
+#endif
+
+-- | @fetchProxy flg@ gets the local proxy settings and parse the string
+-- into a @Proxy@ value. If you want to be informed of ill-formed proxy
+-- configuration strings, supply @True@ for @flg@.
+-- Proxy settings are sourced from the @HTTP_PROXY@ environment variable,
+-- and in the case of Windows platforms, by consulting IE/WinInet's proxy
+-- setting in the Registry.
+fetchProxy :: Bool -> IO Proxy
+fetchProxy warnIfIllformed = do
+  mstr <- proxyString
+  case mstr of
+    Nothing     -> return NoProxy
+    Just str    -> case parseProxy str of
+        Just p  -> return p
+        Nothing -> do
+            when warnIfIllformed $ System.IO.hPutStrLn System.IO.stderr $ unlines
+                    [ "invalid http proxy uri: " ++ show str
+                    , "proxy uri must be http with a hostname"
+                    , "ignoring http proxy, trying a direct connection"
+                    ]
+            return NoProxy
+
+-- | @parseProxy str@ translates a proxy server string into a @Proxy@ value;
+-- returns @Nothing@ if not well-formed.
+parseProxy :: String -> Maybe Proxy
+parseProxy "" = Nothing
+parseProxy str = join
+                   . fmap uri2proxy
+                   $ parseHttpURI str
+             `mplus` parseHttpURI ("http://" ++ str)
+  where
+   parseHttpURI str' =
+    case parseAbsoluteURI str' of
+      Just uri@URI{uriAuthority = Just{}} -> Just (fixUserInfo uri)
+      _  -> Nothing
+
+     -- Note: we need to be able to parse non-URIs like @\"wwwcache.example.com:80\"@
+     -- which lack the @\"http://\"@ URI scheme. The problem is that
+     -- @\"wwwcache.example.com:80\"@ is in fact a valid URI but with scheme
+     -- @\"wwwcache.example.com:\"@, no authority part and a path of @\"80\"@.
+     --
+     -- So our strategy is to try parsing as normal uri first and if it lacks the
+     -- 'uriAuthority' then we try parsing again with a @\"http://\"@ prefix.
+     --
+
+-- | tidy up user portion, don't want the trailing "\@".
+fixUserInfo :: URI -> URI
+fixUserInfo uri = uri{ uriAuthority = f `fmap` uriAuthority uri }
+  where
+   f a@URIAuth{uriUserInfo=s} = a{uriUserInfo=dropWhileTail (=='@') s}
+
+--
+uri2proxy :: URI -> Maybe Proxy
+uri2proxy uri@URI{ uriScheme    = "http:"
+                 , uriAuthority = Just (URIAuth auth' hst prt)
+                 } =
+ Just (Proxy (hst ++ prt) auth)
+  where
+   auth =
+     case auth' of
+       [] -> Nothing
+       as -> Just (AuthBasic "" (unEscapeString usr) (unEscapeString pwd) uri)
+        where
+         (usr,pwd) = chopAtDelim ':' as
+
+uri2proxy _ = Nothing
+
+-- utilities
+#if defined(WIN32)
+regQueryValueDWORD :: HKEY -> String -> IO DWORD
+regQueryValueDWORD hkey name = alloca $ \ptr -> do
+  -- TODO: this throws away the key type returned by regQueryValueEx
+  -- we should check it's what we expect instead
+  _ <- regQueryValueEx hkey name (castPtr ptr) (sizeOf (undefined :: DWORD))
+  peek ptr
+
+#endif
diff --git a/Network/HTTP/Stream.hs b/Network/HTTP/Stream.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/Stream.hs
@@ -0,0 +1,236 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.Stream
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- Transmitting HTTP requests and responses holding @String@ in their payload bodies.
+-- This is one of the implementation modules for the "Network.HTTP" interface, representing
+-- request and response content as @String@s and transmitting them in non-packed form
+-- (cf. "Network.HTTP.HandleStream" and its use of @ByteString@s.) over 'Stream' handles.
+-- It is mostly here for backwards compatibility, representing how requests and responses
+-- were transmitted up until the 4.x releases of the HTTP package.
+--
+-- For more detailed information about what the individual exports do, please consult
+-- the documentation for "Network.HTTP". /Notice/ however that the functions here do
+-- not perform any kind of normalization prior to transmission (or receipt); you are
+-- responsible for doing any such yourself, or, if you prefer, just switch to using
+-- "Network.HTTP" function instead.
+--
+-----------------------------------------------------------------------------
+module Network.HTTP.Stream
+       ( module Network.Stream
+
+       , simpleHTTP      -- :: Request_String -> IO (Result Response_String)
+       , simpleHTTP_     -- :: Stream s => s -> Request_String -> IO (Result Response_String)
+       , sendHTTP        -- :: Stream s => s -> Request_String -> IO (Result Response_String)
+       , sendHTTP_notify -- :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String)
+       , receiveHTTP     -- :: Stream s => s -> IO (Result Request_String)
+       , respondHTTP     -- :: Stream s => s -> Response_String -> IO ()
+
+       ) where
+
+-----------------------------------------------------------------
+------------------ Imports --------------------------------------
+-----------------------------------------------------------------
+
+import Network.Stream
+import Network.StreamDebugger (debugStream)
+import Network.TCP (openTCPPort)
+import Network.BufferType ( stringBufferOp )
+
+import Network.HTTP.Base
+import Network.HTTP.Headers
+import Network.HTTP.Utils ( trim )
+
+import Data.Char     (toLower)
+import Data.Maybe    (fromMaybe)
+import Control.Exception (onException)
+import Control.Monad (when)
+
+
+-- Turn on to enable HTTP traffic logging
+debug :: Bool
+debug = False
+
+-- File that HTTP traffic logs go to
+httpLogFile :: String
+httpLogFile = "http-debug.log"
+
+-----------------------------------------------------------------
+------------------ Misc -----------------------------------------
+-----------------------------------------------------------------
+
+
+-- | Simple way to transmit a resource across a non-persistent connection.
+simpleHTTP :: Request_String -> IO (Result Response_String)
+simpleHTTP r = do
+   auth <- getAuth r
+   c    <- openTCPPort (host auth) (fromMaybe 80 (port auth))
+   simpleHTTP_ c r
+
+-- | Like 'simpleHTTP', but acting on an already opened stream.
+simpleHTTP_ :: Stream s => s -> Request_String -> IO (Result Response_String)
+simpleHTTP_ s r
+ | not debug    = sendHTTP s r
+ | otherwise    = do
+      s' <- debugStream httpLogFile s
+      sendHTTP s' r
+
+sendHTTP :: Stream s => s -> Request_String -> IO (Result Response_String)
+sendHTTP conn rq = sendHTTP_notify conn rq (return ())
+
+sendHTTP_notify :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String)
+sendHTTP_notify conn rq onSendComplete = do
+   when providedClose $ (closeOnEnd conn True)
+   onException (sendMain conn rq onSendComplete)
+               (close conn)
+ where
+  providedClose = findConnClose (rqHeaders rq)
+
+-- From RFC 2616, section 8.2.3:
+-- 'Because of the presence of older implementations, the protocol allows
+-- ambiguous situations in which a client may send "Expect: 100-
+-- continue" without receiving either a 417 (Expectation Failed) status
+-- or a 100 (Continue) status. Therefore, when a client sends this
+-- header field to an origin server (possibly via a proxy) from which it
+-- has never seen a 100 (Continue) status, the client SHOULD NOT wait
+-- for an indefinite period before sending the request body.'
+--
+-- Since we would wait forever, I have disabled use of 100-continue for now.
+sendMain :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String)
+sendMain conn rqst onSendComplete =  do
+    --let str = if null (rqBody rqst)
+    --              then show rqst
+    --              else show (insertHeader HdrExpect "100-continue" rqst)
+    -- TODO review throwing away of result
+   _ <- writeBlock conn (show rqst)
+    -- write body immediately, don't wait for 100 CONTINUE
+   -- TODO review throwing away of result
+   _ <- writeBlock conn (rqBody rqst)
+   onSendComplete
+   rsp <- getResponseHead conn
+   switchResponse conn True False rsp rqst
+
+-- reads and parses headers
+getResponseHead :: Stream s => s -> IO (Result ResponseData)
+getResponseHead conn = do
+   lor <- readTillEmpty1 stringBufferOp (readLine conn)
+   return $ lor >>= parseResponseHead
+
+-- Hmmm, this could go bad if we keep getting "100 Continue"
+-- responses...  Except this should never happen according
+-- to the RFC.
+switchResponse :: Stream s
+               => s
+               -> Bool {- allow retry? -}
+               -> Bool {- is body sent? -}
+               -> Result ResponseData
+               -> Request_String
+               -> IO (Result Response_String)
+switchResponse _ _ _ (Left e) _ = return (Left e)
+        -- retry on connreset?
+        -- if we attempt to use the same socket then there is an excellent
+        -- chance that the socket is not in a completely closed state.
+switchResponse conn allow_retry bdy_sent (Right (cd,rn,hdrs)) rqst =
+            case matchResponse (rqMethod rqst) cd of
+                Continue
+                    | not bdy_sent -> {- Time to send the body -}
+                        do { val <- writeBlock conn (rqBody rqst)
+                           ; case val of
+                                Left e -> return (Left e)
+                                Right _ ->
+                                    do { rsp <- getResponseHead conn
+                                       ; switchResponse conn allow_retry True rsp rqst
+                                       }
+                           }
+                    | otherwise -> {- keep waiting -}
+                        do { rsp <- getResponseHead conn
+                           ; switchResponse conn allow_retry bdy_sent rsp rqst
+                           }
+
+                Retry -> {- Request with "Expect" header failed.
+                                Trouble is the request contains Expects
+                                other than "100-Continue" -}
+                    do { -- TODO review throwing away of result
+                         _ <- writeBlock conn (show rqst ++ rqBody rqst)
+                       ; rsp <- getResponseHead conn
+                       ; switchResponse conn False bdy_sent rsp rqst
+                       }
+
+                Done -> do
+                    when (findConnClose hdrs)
+                         (closeOnEnd conn True)
+                    return (Right $ Response cd rn hdrs "")
+
+                DieHorribly str -> do
+                    close conn
+                    return $ responseParseError "sendHTTP" ("Invalid response: " ++ str)
+
+                ExpectEntity ->
+                    let tc = lookupHeader HdrTransferEncoding hdrs
+                        cl = lookupHeader HdrContentLength hdrs
+                    in
+                    do { rslt <- case tc of
+                          Nothing ->
+                              case cl of
+                                  Just x  -> linearTransfer (readBlock conn) (read x :: Int)
+                                  Nothing -> hopefulTransfer stringBufferOp {-null (++) []-} (readLine conn) []
+                          Just x  ->
+                              case map toLower (trim x) of
+                                  "chunked" -> chunkedTransfer stringBufferOp
+                                                               (readLine conn) (readBlock conn)
+                                  _         -> uglyDeathTransfer "sendHTTP"
+                       ; case rslt of
+                           Left e -> close conn >> return (Left e)
+                           Right (ftrs,bdy) -> do
+                            when (findConnClose (hdrs++ftrs))
+                                 (closeOnEnd conn True)
+                            return (Right (Response cd rn (hdrs++ftrs) bdy))
+                       }
+
+-- | Receive and parse a HTTP request from the given Stream. Should be used
+--   for server side interactions.
+receiveHTTP :: Stream s => s -> IO (Result Request_String)
+receiveHTTP conn = getRequestHead >>= processRequest
+    where
+        -- reads and parses headers
+        getRequestHead :: IO (Result RequestData)
+        getRequestHead =
+            do { lor <- readTillEmpty1 stringBufferOp (readLine conn)
+               ; return $ lor >>= parseRequestHead
+               }
+
+        processRequest (Left e) = return $ Left e
+        processRequest (Right (rm,uri,hdrs)) =
+            do -- FIXME : Also handle 100-continue.
+               let tc = lookupHeader HdrTransferEncoding hdrs
+                   cl = lookupHeader HdrContentLength hdrs
+               rslt <- case tc of
+                          Nothing ->
+                              case cl of
+                                  Just x  -> linearTransfer (readBlock conn) (read x :: Int)
+                                  Nothing -> return (Right ([], "")) -- hopefulTransfer ""
+                          Just x  ->
+                              case map toLower (trim x) of
+                                  "chunked" -> chunkedTransfer stringBufferOp
+                                                               (readLine conn) (readBlock conn)
+                                  _         -> uglyDeathTransfer "receiveHTTP"
+
+               return $ do
+                  (ftrs,bdy) <- rslt
+                  return (Request uri rm (hdrs++ftrs) bdy)
+
+-- | Very simple function, send a HTTP response over the given stream. This
+--   could be improved on to use different transfer types.
+respondHTTP :: Stream s => s -> Response_String -> IO ()
+respondHTTP conn rsp = do -- TODO review throwing away of result
+                          _ <- writeBlock conn (show rsp)
+                          -- write body immediately, don't wait for 100 CONTINUE
+                          -- TODO review throwing away of result
+                          _ <- writeBlock conn (rspBody rsp)
+                          return ()
diff --git a/Network/HTTP/Utils.hs b/Network/HTTP/Utils.hs
new file mode 100644
--- /dev/null
+++ b/Network/HTTP/Utils.hs
@@ -0,0 +1,207 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.HTTP.Utils
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- Set of utility functions and definitions used by package modules.
+--
+module Network.HTTP.Utils
+       ( trim     -- :: String -> String
+       , trimL    -- :: String -> String
+       , trimR    -- :: String -> String
+
+       , crlf     -- :: String
+       , lf       -- :: String
+       , sp       -- :: String
+
+       , split    -- :: Eq a => a -> [a] -> Maybe ([a],[a])
+       , splitBy  -- :: Eq a => a -> [a] -> [[a]]
+
+       , readsOne -- :: Read a => (a -> b) -> b -> String -> b
+
+       , dropWhileTail -- :: (a -> Bool) -> [a] -> [a]
+       , chopAtDelim   -- :: Eq a => a -> [a] -> ([a],[a])
+
+       , toUTF8BS
+       , fromUTF8BS
+       ) where
+
+import Data.Bits
+import Data.Char
+import Data.List ( elemIndex )
+import Data.Maybe ( fromMaybe )
+import Data.Word ( Word8 )
+
+import qualified Data.ByteString as BS
+
+-- | @crlf@ is our beloved two-char line terminator.
+crlf :: String
+crlf = "\r\n"
+
+-- | @lf@ is a tolerated line terminator, per RFC 2616 section 19.3.
+lf :: String
+lf = "\n"
+
+-- | @sp@ lets you save typing one character.
+sp :: String
+sp   = " "
+
+-- | @split delim ls@ splits a list into two parts, the @delim@ occurring
+-- at the head of the second list.  If @delim@ isn't in @ls@, @Nothing@ is
+-- returned.
+split :: Eq a => a -> [a] -> Maybe ([a],[a])
+split delim list = case delim `elemIndex` list of
+    Nothing -> Nothing
+    Just x  -> Just $ splitAt x list
+
+-- | @trim str@ removes leading and trailing whitespace from @str@.
+trim :: String -> String
+trim xs = trimR (trimL xs)
+
+-- | @trimL str@ removes leading whitespace (as defined by 'Data.Char.isSpace')
+-- from @str@.
+trimL :: String -> String
+trimL xs = dropWhile isSpace xs
+
+-- | @trimL str@ removes trailing whitespace (as defined by 'Data.Char.isSpace')
+-- from @str@.
+trimR :: String -> String
+trimR str = fromMaybe "" $ foldr trimIt Nothing str
+ where
+  trimIt x (Just xs) = Just (x:xs)
+  trimIt x Nothing
+   | isSpace x = Nothing
+   | otherwise = Just [x]
+
+-- | @splitMany delim ls@ removes the delimiter @delim@ from @ls@.
+splitBy :: Eq a => a -> [a] -> [[a]]
+splitBy _ [] = []
+splitBy c xs =
+    case break (==c) xs of
+      (_,[]) -> [xs]
+      (as,_:bs) -> as : splitBy c bs
+
+-- | @readsOne f def str@ tries to 'read' @str@, taking
+-- the first result and passing it to @f@. If the 'read'
+-- doesn't succeed, return @def@.
+readsOne :: Read a => (a -> b) -> b -> String -> b
+readsOne f n str =
+ case reads str of
+   ((v,_):_) -> f v
+   _ -> n
+
+
+-- | @dropWhileTail p ls@ chops off trailing elements from @ls@
+-- until @p@ returns @False@.
+dropWhileTail :: (a -> Bool) -> [a] -> [a]
+dropWhileTail f ls =
+ case foldr chop Nothing ls of { Just xs -> xs; Nothing -> [] }
+  where
+    chop x (Just xs) = Just (x:xs)
+    chop x _
+     | f x       = Nothing
+     | otherwise = Just [x]
+
+-- | @chopAtDelim elt ls@ breaks up @ls@ into two at first occurrence
+-- of @elt@; @elt@ is elided too. If @elt@ does not occur, the second
+-- list is empty and the first is equal to @ls@.
+chopAtDelim :: Eq a => a -> [a] -> ([a],[a])
+chopAtDelim elt xs =
+  case break (==elt) xs of
+    (_,[])    -> (xs,[])
+    (as,_:bs) -> (as,bs)
+
+toUTF8BS :: String -> BS.ByteString
+toUTF8BS = BS.pack . encodeStringUtf8
+
+fromUTF8BS :: BS.ByteString -> String
+fromUTF8BS = decodeStringUtf8 . BS.unpack
+
+-- | Encode 'String' to a list of UTF8-encoded octets
+--
+-- Code-points in the @U+D800@-@U+DFFF@ range will be encoded
+-- as the replacement character (i.e. @U+FFFD@).
+--
+-- The code is extracted from Cabal library, written originally
+-- Herbert Valerio Riedel under BSD-3-Clause license
+encodeStringUtf8 :: String -> [Word8]
+encodeStringUtf8 []        = []
+encodeStringUtf8 (c:cs)
+  | c <= '\x07F' = w8
+                 : encodeStringUtf8 cs
+  | c <= '\x7FF' = (0xC0 .|.  w8ShiftR  6          )
+                 : (0x80 .|. (w8          .&. 0x3F))
+                 : encodeStringUtf8 cs
+  | c <= '\xD7FF'= (0xE0 .|.  w8ShiftR 12          )
+                 : (0x80 .|. (w8ShiftR  6 .&. 0x3F))
+                 : (0x80 .|. (w8          .&. 0x3F))
+                 : encodeStringUtf8 cs
+  | c <= '\xDFFF'= 0xEF : 0xBF : 0xBD -- U+FFFD
+                 : encodeStringUtf8 cs
+  | c <= '\xFFFF'= (0xE0 .|.  w8ShiftR 12          )
+                 : (0x80 .|. (w8ShiftR  6 .&. 0x3F))
+                 : (0x80 .|. (w8          .&. 0x3F))
+                 : encodeStringUtf8 cs
+  | otherwise    = (0xf0 .|.  w8ShiftR 18          )
+                 : (0x80 .|. (w8ShiftR 12 .&. 0x3F))
+                 : (0x80 .|. (w8ShiftR  6 .&. 0x3F))
+                 : (0x80 .|. (w8          .&. 0x3F))
+                 : encodeStringUtf8 cs
+  where
+    w8 = fromIntegral (ord c) :: Word8
+    w8ShiftR :: Int -> Word8
+    w8ShiftR = fromIntegral . shiftR (ord c)
+
+-- | Decode 'String' from UTF8-encoded octets.
+--
+-- Invalid data in the UTF8 stream (this includes code-points @U+D800@
+-- through @U+DFFF@) will be decoded as the replacement character (@U+FFFD@).
+--
+-- See also 'encodeStringUtf8'
+decodeStringUtf8 :: [Word8] -> String
+decodeStringUtf8 = go
+  where
+    go :: [Word8] -> String
+    go []       = []
+    go (c : cs)
+      | c <= 0x7F = chr (fromIntegral c) : go cs
+      | c <= 0xBF = replacementChar : go cs
+      | c <= 0xDF = twoBytes c cs
+      | c <= 0xEF = moreBytes 3 0x800     cs (fromIntegral $ c .&. 0xF)
+      | c <= 0xF7 = moreBytes 4 0x10000   cs (fromIntegral $ c .&. 0x7)
+      | c <= 0xFB = moreBytes 5 0x200000  cs (fromIntegral $ c .&. 0x3)
+      | c <= 0xFD = moreBytes 6 0x4000000 cs (fromIntegral $ c .&. 0x1)
+      | otherwise   = replacementChar : go cs
+
+    twoBytes :: Word8 -> [Word8] -> String
+    twoBytes c0 (c1:cs')
+      | c1 .&. 0xC0 == 0x80
+      = let d = (fromIntegral (c0 .&. 0x1F) `shiftL` 6)
+             .|. fromIntegral (c1 .&. 0x3F)
+         in if d >= 0x80
+               then  chr d                : go cs'
+               else  replacementChar      : go cs'
+    twoBytes _ cs' = replacementChar      : go cs'
+
+    moreBytes :: Int -> Int -> [Word8] -> Int -> [Char]
+    moreBytes 1 overlong cs' acc
+      | overlong <= acc && acc <= 0x10FFFF && (acc < 0xD800 || 0xDFFF < acc)
+      = chr acc : go cs'
+
+      | otherwise
+      = replacementChar : go cs'
+
+    moreBytes byteCount overlong (cn:cs') acc
+      | cn .&. 0xC0 == 0x80
+      = moreBytes (byteCount-1) overlong cs'
+          ((acc `shiftL` 6) .|. fromIntegral cn .&. 0x3F)
+
+    moreBytes _ _ cs' _
+      = replacementChar : go cs'
+
+    replacementChar = '\xfffd'
diff --git a/Network/Stream.hs b/Network/Stream.hs
--- a/Network/Stream.hs
+++ b/Network/Stream.hs
@@ -1,68 +1,71 @@
 -----------------------------------------------------------------------------
 -- |
 -- Module      :  Network.Stream
--- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004
+-- Copyright   :  See LICENSE file
 -- License     :  BSD
 --
--- Maintainer  :  bjorn@bringert.net
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
 -- Stability   :  experimental
 -- Portability :  non-portable (not tested)
 --
 -- An library for creating abstract streams. Originally part of Gray's\/Bringert's
 -- HTTP module.
 --
+-- * Changes by Robin Bate Boerop <robin@bateboerop.name>:
+--      - Removed unnecessary import statements.
+--      - Moved Debug code to StreamDebugger.hs
+--      - Moved Socket-related code to StreamSocket.hs.
+--
 -- * Changes by Simon Foster:
---      - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules
---      
+--      - Split Network.HTTPmodule up into to separate
+--        Network.[Stream,TCP,HTTP] modules
 -----------------------------------------------------------------------------
-module Network.Stream (
-    -- ** Streams
-    Debug,
-    Stream(..),
-    debugStream,
-    
-    -- ** Errors
-    ConnError(..),
-    Result,
-    handleSocketError,
-    bindE,
-    myrecv
+module Network.Stream
+   ( Stream(..)
+   , ConnError(..)
+   , Result
+   , bindE
+   , fmapE
 
-) where
+   , failParse -- :: String -> Result a
+   , failWith  -- :: ConnError -> Result a
+   , failMisc  -- :: String -> Result a
+   ) where
 
-import Control.Exception as Exception
-import System.IO.Error
+data ConnError
+ = ErrorReset
+ | ErrorClosed
+ | ErrorParse String
+ | ErrorMisc String
+   deriving(Show,Eq)
 
--- Networking
-import Network (withSocketsDo)
-import Network.BSD
-import Network.URI
-import Network.Socket
+-- in GHC 7.0 the Monad instance for Error no longer
+-- uses fail x = Left (strMsg x). failMisc is therefore
+-- used instead.
+failMisc :: String -> Result a
+failMisc x = failWith (ErrorMisc x)
 
-import Control.Monad (when,liftM,guard)
-import System.IO
+failParse :: String -> Result a
+failParse x = failWith (ErrorParse x)
 
-data ConnError = ErrorReset 
-               | ErrorClosed
-               | ErrorParse String
-               | ErrorMisc String
-    deriving(Show,Eq)
+failWith :: ConnError -> Result a
+failWith x = Left x
 
--- error propagating:
--- we could've used a monad, but that would lead us
--- into using the "-fglasgow-exts" compile flag.
-bindE :: Either ConnError a -> (a -> Either ConnError b) -> Either ConnError b
+bindE :: Result a -> (a -> Result b) -> Result b
 bindE (Left e)  _ = Left e
 bindE (Right v) f = f v
 
+fmapE :: (a -> Result b) -> IO (Result a) -> IO (Result b)
+fmapE f a = do
+ x <- a
+ case x of
+   Left  e -> return (Left e)
+   Right r -> return (f r)
+
 -- | This is the type returned by many exported network functions.
 type Result a = Either ConnError   {- error  -}
                        a           {- result -}
 
------------------------------------------------------------------
------------------- Gentle Art of Socket Sucking -----------------
------------------------------------------------------------------
-
 -- | Streams should make layering of TLS protocol easier in future,
 -- they allow reading/writing to files etc for debugging,
 -- they allow use of protocols other than TCP/IP
@@ -72,104 +75,11 @@
 -- the input in any way, e.g. leave LF on line
 -- endings etc. Unless that is exactly the behaviour
 -- you want from your twisted instances ;)
-class Stream x where 
+class Stream x where
     readLine   :: x -> IO (Result String)
     readBlock  :: x -> Int -> IO (Result String)
     writeBlock :: x -> String -> IO (Result ())
     close      :: x -> IO ()
-
-
-
-
-
--- Exception handler for socket operations
-handleSocketError :: Socket -> Exception -> IO (Result a)
-handleSocketError sk e =
-    do { se <- getSocketOption sk SoError
-       ; if se == 0
-            then throw e
-            else return $ if se == 10054       -- reset
-                then Left ErrorReset
-                else Left $ ErrorMisc $ show se
-       }
-
-
-
-
-instance Stream Socket where
-    readBlock sk n = (liftM Right $ fn n) `Exception.catch` (handleSocketError sk)
-        where
-            fn x = do { str <- myrecv sk x
-                      ; let len = length str
-                      ; if len < x
-                          then ( fn (x-len) >>= \more -> return (str++more) )                        
-                          else return str
-                      }
-
-    -- Use of the following function is discouraged.
-    -- The function reads in one character at a time, 
-    -- which causes many calls to the kernel recv()
-    -- hence causes many context switches.
-    readLine sk = (liftM Right $ fn "") `Exception.catch` (handleSocketError sk)
-            where
-                fn str =
-                    do { c <- myrecv sk 1 -- like eating through a straw.
-                       ; if null c || c == "\n"
-                           then return (reverse str++c)
-                           else fn (head c:str)
-                       }
-    
-    writeBlock sk str = (liftM Right $ fn str) `Exception.catch` (handleSocketError sk)
-        where
-            fn [] = return ()
-            fn x  = send sk x >>= \i -> fn (drop i x)
-
-    -- This slams closed the connection (which is considered rude for TCP\/IP)
-    close sk = shutdown sk ShutdownBoth >> sClose sk
-
-myrecv :: Socket -> Int -> IO String
-myrecv sock len =
-    let handler e = if isEOFError e then return [] else ioError e
-        in System.IO.Error.catch (recv sock len) handler
-
--- | Allows stream logging.
--- Refer to 'debugStream' below.
-data Debug x = Dbg Handle x
-
-
-instance (Stream x) => Stream (Debug x) where
-    readBlock (Dbg h c) n =
-        do { val <- readBlock c n
-           ; hPutStrLn h ("readBlock " ++ show n ++ ' ' : show val)
-           ; return val
-           }
-
-    readLine (Dbg h c) =
-        do { val <- readLine c
-           ; hPutStrLn h ("readLine " ++ show val)
-           ; return val
-           }
-
-    writeBlock (Dbg h c) str =
-        do { val <- writeBlock c str
-           ; hPutStrLn h ("writeBlock " ++ show val ++ ' ' : show str)
-           ; return val
-           }
-
-    close (Dbg h c) =
-        do { hPutStrLn h "closing..."
-           ; hFlush h
-           ; close c
-           ; hPutStrLn h "...closed"
-           ; hClose h
-           }
-
-
--- | Wraps a stream with logging I\/O, the first
--- argument is a filename which is opened in AppendMode.
-debugStream :: (Stream a) => String -> a -> IO (Debug a)
-debugStream file stm = 
-    do { h <- openFile file AppendMode
-       ; hPutStrLn h "File opened for appending."
-       ; return (Dbg h stm)
-       }
+    closeOnEnd :: x -> Bool -> IO ()
+      -- ^ True => shutdown the connection when response has been read / end-of-stream
+      --           has been reached.
diff --git a/Network/StreamDebugger.hs b/Network/StreamDebugger.hs
new file mode 100644
--- /dev/null
+++ b/Network/StreamDebugger.hs
@@ -0,0 +1,103 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.StreamDebugger
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- Implements debugging of @Stream@s.  Originally part of Gray's\/Bringert's
+-- HTTP module.
+--
+-- * Changes by Robin Bate Boerop <robin@bateboerop.name>:
+--      - Created.  Made minor formatting changes.
+--
+-----------------------------------------------------------------------------
+module Network.StreamDebugger
+   ( StreamDebugger
+   , debugStream
+   , debugByteStream
+   ) where
+
+import Network.Stream (Stream(..))
+import System.IO
+   ( Handle, hFlush, hPutStrLn, IOMode(AppendMode), hClose, openFile,
+     hSetBuffering, BufferMode(NoBuffering)
+   )
+import Network.TCP ( HandleStream, HStream,
+                     StreamHooks(..), setStreamHooks, getStreamHooks )
+
+-- | Allows stream logging.  Refer to 'debugStream' below.
+data StreamDebugger x
+   = Dbg Handle x
+
+instance (Stream x) => Stream (StreamDebugger x) where
+    readBlock (Dbg h x) n =
+        do val <- readBlock x n
+           hPutStrLn h ("--readBlock " ++ show n)
+           hPutStrLn h (show val)
+           return val
+    readLine (Dbg h x) =
+        do val <- readLine x
+           hPutStrLn h ("--readLine")
+           hPutStrLn h (show val)
+           return val
+    writeBlock (Dbg h x) str =
+        do val <- writeBlock x str
+           hPutStrLn h ("--writeBlock" ++ show str)
+           hPutStrLn h (show val)
+           return val
+    close (Dbg h x) =
+        do hPutStrLn h "--closing..."
+           hFlush h
+           close x
+           hPutStrLn h "--closed."
+           hClose h
+    closeOnEnd (Dbg h x) f =
+        do hPutStrLn h ("--close-on-end.." ++ show f)
+           hFlush h
+           closeOnEnd x f
+
+-- | Wraps a stream with logging I\/O.
+--   The first argument is a filename which is opened in @AppendMode@.
+debugStream :: (Stream a) => FilePath -> a -> IO (StreamDebugger a)
+debugStream file stream =
+    do h <- openFile file AppendMode
+       hPutStrLn h ("File \"" ++ file ++ "\" opened for appending.")
+       return (Dbg h stream)
+
+debugByteStream :: HStream ty => FilePath -> HandleStream ty -> IO (HandleStream ty)
+debugByteStream file stream = do
+   sh <- getStreamHooks stream
+   case sh of
+     Just h
+      | hook_name h == file -> return stream -- reuse the stream hooks.
+     _ -> do
+       h <- openFile file AppendMode
+       hSetBuffering h NoBuffering
+       hPutStrLn h ("File \"" ++ file ++ "\" opened for appending.")
+       setStreamHooks stream (debugStreamHooks h file)
+       return stream
+
+debugStreamHooks :: HStream ty => Handle -> String -> StreamHooks ty
+debugStreamHooks h nm =
+  StreamHooks
+    { hook_readBlock = \ toStr n val -> do
+       let eval = case val of { Left e -> Left e ; Right v -> Right $ toStr v}
+       hPutStrLn h ("--readBlock " ++ show n)
+       hPutStrLn h (either show show eval)
+    , hook_readLine = \ toStr val -> do
+           let eval = case val of { Left e -> Left e ; Right v -> Right $ toStr v}
+           hPutStrLn h ("--readLine")
+           hPutStrLn h (either show show eval)
+    , hook_writeBlock = \ toStr str val -> do
+           hPutStrLn h ("--writeBlock " ++ show val)
+           hPutStrLn h (toStr str)
+    , hook_close = do
+           hPutStrLn h "--closing..."
+           hFlush h
+           hClose h
+    , hook_name = nm
+    }
diff --git a/Network/StreamSocket.hs b/Network/StreamSocket.hs
new file mode 100644
--- /dev/null
+++ b/Network/StreamSocket.hs
@@ -0,0 +1,97 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Network.StreamSocket
+-- Copyright   :  See LICENSE file
+-- License     :  BSD
+--
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
+-- Stability   :  experimental
+-- Portability :  non-portable (not tested)
+--
+-- Socket Stream instance. Originally part of Gray's\/Bringert's HTTP module.
+--
+-- * Changes by Robin Bate Boerop <robin@bateboerop.name>:
+--      - Made dependencies explicit in import statements.
+--      - Removed false dependencies in import statements.
+--      - Created separate module for instance Stream Socket.
+--
+-- * Changes by Simon Foster:
+--      - Split module up into to separate Network.[Stream,TCP,HTTP] modules
+--
+-----------------------------------------------------------------------------
+module Network.StreamSocket
+   ( handleSocketError
+   , myrecv
+   ) where
+
+import Network.Stream
+   ( Stream(..), ConnError(ErrorReset, ErrorMisc), Result
+   )
+import Network.Socket
+   ( Socket, getSocketOption, shutdown
+   , ShutdownCmd(ShutdownBoth), SocketOption(SoError)
+   )
+import Network.Socket.ByteString (send, recv)
+import qualified Network.Socket
+   ( close )
+
+import Network.HTTP.Base ( catchIO )
+import Network.HTTP.Utils ( fromUTF8BS, toUTF8BS )
+import Control.Monad (liftM)
+import Control.Exception as Exception (IOException)
+import System.IO.Error (isEOFError)
+
+-- | Exception handler for socket operations.
+handleSocketError :: Socket -> IOException -> IO (Result a)
+handleSocketError sk e =
+    do se <- getSocketOption sk SoError
+       case se of
+          0     -> ioError e
+          10054 -> return $ Left ErrorReset  -- reset
+          _     -> return $ Left $ ErrorMisc $ show se
+
+myrecv :: Socket -> Int -> IO String
+myrecv sock len =
+    let handler e = if isEOFError e then return [] else ioError e
+        in catchIO (fmap fromUTF8BS (recv sock len)) handler
+
+instance Stream Socket where
+    readBlock sk n    = readBlockSocket sk n
+    readLine sk       = readLineSocket sk
+    writeBlock sk str = writeBlockSocket sk str
+    close sk          = do
+        -- This slams closed the connection (which is considered rude for TCP\/IP)
+         shutdown sk ShutdownBoth
+         Network.Socket.close sk
+    closeOnEnd _sk _  = return () -- can't really deal with this, so do run the risk of leaking sockets here.
+
+readBlockSocket :: Socket -> Int -> IO (Result String)
+readBlockSocket sk n = (liftM Right $ fn n) `catchIO` (handleSocketError sk)
+  where
+   fn x = do { str <- myrecv sk x
+             ; let len = length str
+             ; if len < x
+                then ( fn (x-len) >>= \more -> return (str++more) )
+                else return str
+             }
+
+-- Use of the following function is discouraged.
+-- The function reads in one character at a time,
+-- which causes many calls to the kernel recv()
+-- hence causes many context switches.
+readLineSocket :: Socket -> IO (Result String)
+readLineSocket sk = (liftM Right $ fn "") `catchIO` (handleSocketError sk)
+  where
+   fn str = do
+     c <- myrecv sk 1 -- like eating through a straw.
+     if null c || c == "\n"
+      then return (reverse str++c)
+      else fn (head c:str)
+
+writeBlockSocket :: Socket -> String -> IO (Result ())
+writeBlockSocket sk str = (liftM Right $ fn str) `catchIO` (handleSocketError sk)
+  where
+   fn [] = return ()
+   fn x  = send sk (toUTF8BS x) >>= \i -> fn (drop i x)
+
diff --git a/Network/TCP.hs b/Network/TCP.hs
--- a/Network/TCP.hs
+++ b/Network/TCP.hs
@@ -1,194 +1,439 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeSynonymInstances #-}
 -----------------------------------------------------------------------------
 -- |
 -- Module      :  Network.TCP
--- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004
+-- Copyright   :  See LICENSE file
 -- License     :  BSD
 --
--- Maintainer  :  bjorn@bringert.net
+-- Maintainer  :  Ganesh Sittampalam <ganesh@earth.li>
 -- Stability   :  experimental
 -- Portability :  non-portable (not tested)
 --
--- An easy access TCP library. Makes the use of TCP in Haskell much easier.
--- This was originally part of Gray's\/Bringert's HTTP module.
+-- Some utility functions for working with the Haskell @network@ package. Mostly
+-- for internal use by the @Network.HTTP@ code.
 --
--- * Changes by Simon Foster:
---      - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules
---      
 -----------------------------------------------------------------------------
-module Network.TCP (
-    -- ** Connections
-    Conn(..),
-    Connection(..),
-    openTCP,
-    openTCPPort,
-    isConnectedTo
-) where
+module Network.TCP
+   ( Connection
+   , EndPoint(..)
+   , openTCPPort
+   , isConnectedTo
 
-import Control.Exception as Exception
+   , openTCPConnection
+   , socketConnection
+   , isTCPConnectedTo
 
--- Networking
-import Network (withSocketsDo)
-import Network.BSD
-import Network.URI
+   , HandleStream
+   , HStream(..)
+
+   , StreamHooks(..)
+   , nullHooks
+   , setStreamHooks
+   , getStreamHooks
+   , hstreamToConnection
+
+   ) where
+
 import Network.Socket
+   ( Socket, SocketOption(KeepAlive)
+   , SocketType(Stream), connect
+   , shutdown, ShutdownCmd(..)
+   , setSocketOption, getPeerName
+   , socket, Family(AF_UNSPEC), defaultProtocol, getAddrInfo
+   , defaultHints, addrFamily, withSocketsDo
+   , addrSocketType, addrAddress
+   )
+import qualified Network.Socket
+   ( close )
+import qualified Network.Stream as Stream
+   ( Stream(readBlock, readLine, writeBlock, close, closeOnEnd) )
 import Network.Stream
+   ( ConnError(..)
+   , Result
+   , failWith
+   , failMisc
+   )
+import Network.BufferType
 
-import Data.List (isPrefixOf,partition,elemIndex)
-import Data.Char
-import Data.IORef
-import Control.Monad (when,liftM,guard)
-import System.IO
+import Network.HTTP.Base ( catchIO )
+import Network.Socket ( socketToHandle )
 
+import Data.Char  ( toLower )
+import Data.Word  ( Word8 )
+import Control.Concurrent
+import Control.Exception ( IOException, bracketOnError, try )
+import Control.Monad ( liftM, when )
+import System.IO ( Handle, hFlush, IOMode(..), hClose )
+import System.IO.Error ( isEOFError )
+
+import qualified Data.ByteString      as Strict
+import qualified Data.ByteString.Lazy as Lazy
+
 -----------------------------------------------------------------
 ------------------ TCP Connections ------------------------------
 -----------------------------------------------------------------
 
 -- | The 'Connection' newtype is a wrapper that allows us to make
--- connections an instance of the StreamIn\/Out classes, without ghc extensions.
+-- connections an instance of the Stream class, without GHC extensions.
 -- While this looks sort of like a generic reference to the transport
 -- layer it is actually TCP specific, which can be seen in the
 -- implementation of the 'Stream Connection' instance.
-newtype Connection = ConnRef {getRef :: IORef Conn}
+newtype Connection = Connection (HandleStream String)
 
+newtype HandleStream a = HandleStream {getRef :: MVar (Conn a)}
 
--- | The 'Conn' object allows input buffering, and maintenance of 
--- some admin-type data.
-data Conn = MkConn { connSock :: ! Socket
-                   , connAddr :: ! SockAddr 
-                   , connBffr :: ! String 
-                   , connHost :: String
-                   }
-          | ConnClosed
-    deriving(Eq)
+data EndPoint = EndPoint { epHost :: String, epPort :: Int }
 
+instance Eq EndPoint where
+   EndPoint host1 port1 == EndPoint host2 port2 =
+     map toLower host1 == map toLower host2 && port1 == port2
 
--- | Open a connection to port 80 on a remote host.
-openTCP :: String -> IO Connection
-openTCP host = openTCPPort host 80
+data Conn a
+ = MkConn { connSock      :: !Socket
+          , connHandle    :: Handle
+          , connBuffer    :: BufferOp a
+          , connInput     :: Maybe a
+          , connEndPoint  :: EndPoint
+          , connHooks     :: Maybe (StreamHooks a)
+          , connCloseEOF  :: Bool -- True => close socket upon reaching end-of-stream.
+          }
+ | ConnClosed
+   deriving(Eq)
 
+hstreamToConnection :: HandleStream String -> Connection
+hstreamToConnection h = Connection h
 
--- | This function establishes a connection to a remote
--- host, it uses "getHostByName" which interrogates the
--- DNS system, hence may trigger a network connection.
+connHooks' :: Conn a -> Maybe (StreamHooks a)
+connHooks' ConnClosed{} = Nothing
+connHooks' x = connHooks x
+
+-- all of these are post-op hooks
+data StreamHooks ty
+ = StreamHooks
+     { hook_readLine   :: (ty -> String) -> Result ty -> IO ()
+     , hook_readBlock  :: (ty -> String) -> Int -> Result ty -> IO ()
+     , hook_writeBlock :: (ty -> String) -> ty  -> Result () -> IO ()
+     , hook_close      :: IO ()
+     , hook_name       :: String -- hack alert: name of the hook itself.
+     }
+
+instance Eq ty => Eq (StreamHooks ty) where
+  (==) _ _ = True
+
+nullHooks :: StreamHooks ty
+nullHooks = StreamHooks
+     { hook_readLine   = \ _ _   -> return ()
+     , hook_readBlock  = \ _ _ _ -> return ()
+     , hook_writeBlock = \ _ _ _ -> return ()
+     , hook_close      = return ()
+     , hook_name       = ""
+     }
+
+setStreamHooks :: HandleStream ty -> StreamHooks ty -> IO ()
+setStreamHooks h sh = modifyMVar_ (getRef h) (\ c -> return c{connHooks=Just sh})
+
+getStreamHooks :: HandleStream ty -> IO (Maybe (StreamHooks ty))
+getStreamHooks h = readMVar (getRef h) >>= return.connHooks
+
+-- | @HStream@ overloads the use of 'HandleStream's, letting you
+-- overload the handle operations over the type that is communicated
+-- across the handle. It comes in handy for @Network.HTTP@ 'Request'
+-- and 'Response's as the payload representation isn't fixed, but overloaded.
 --
--- Add a "persistant" option?  Current persistant is default.
--- Use "Result" type for synchronous exception reporting?
-openTCPPort :: String -> Int -> IO Connection
-openTCPPort uri port = 
-    do { s <- socket AF_INET Stream 6
-       ; setSocketOption s KeepAlive 1
-       ; host <- Exception.catch (inet_addr uri)    -- handles ascii IP numbers
-                       (\_ -> getHostByName uri >>= \host ->
-                            case hostAddresses host of
-                                [] -> return (error "no addresses in host entry")
-                                (h:_) -> return h)
-       ; let a = SockAddrInet (toEnum port) host
-       ; Exception.catch (connect s a) (\e -> sClose s >> throw e)
-       ; v <- newIORef (MkConn s a [] uri)
-       ; return (ConnRef v)
-       }
+-- The library comes with instances for @ByteString@s and @String@, but
+-- should you want to plug in your own payload representation, defining
+-- your own @HStream@ instance _should_ be all that it takes.
+--
+class BufferType bufType => HStream bufType where
+  openStream       :: String -> Int -> IO (HandleStream bufType)
+  openSocketStream :: String -> Int -> Socket -> IO (HandleStream bufType)
+  readLine         :: HandleStream bufType -> IO (Result bufType)
+  readBlock        :: HandleStream bufType -> Int -> IO (Result bufType)
+  writeBlock       :: HandleStream bufType -> bufType -> IO (Result ())
+  close            :: HandleStream bufType -> IO ()
+  closeQuick       :: HandleStream bufType -> IO ()
+  closeOnEnd       :: HandleStream bufType -> Bool -> IO ()
 
-instance Stream Connection where
-    readBlock ref n = 
-        readIORef (getRef ref) >>= \conn -> case conn of
-            ConnClosed -> return (Left ErrorClosed)
-            (MkConn sk addr bfr hst)
-                | length bfr >= n ->
-                    do { modifyIORef (getRef ref) (\c -> c { connBffr=(drop n bfr) })
-                       ; return (Right $ take n bfr)
-                       }
-                | otherwise ->
-                    do { modifyIORef (getRef ref) (\c -> c { connBffr=[] })
-                       ; more <- readBlock sk (n - length bfr)
-                       ; return $ case more of
-                            Left _ -> more
-                            Right s -> (Right $ bfr ++ s)
-                       }
+instance HStream Strict.ByteString where
+  openStream       = openTCPConnection
+  openSocketStream = socketConnection
+  readBlock c n    = readBlockBS c n
+  readLine c       = readLineBS c
+  writeBlock c str = writeBlockBS c str
+  close c          = closeIt c Strict.null True
+  closeQuick c     = closeIt c Strict.null False
+  closeOnEnd c f   = closeEOF c f
 
-    -- This function uses a buffer, at this time the buffer is just 1000 characters.
-    -- (however many bytes this is is left to the user to decypher)
-    readLine ref =
-        readIORef (getRef ref) >>= \conn -> case conn of
-             ConnClosed -> return (Left ErrorClosed)
-             (MkConn sk addr bfr _)
-                 | null bfr ->  {- read in buffer -}
-                      do { str <- myrecv sk 1000  -- DON'T use "readBlock sk 1000" !!
-                                                -- ... since that call will loop.
-                         ; let len = length str
-                         ; if len == 0   {- indicates a closed connection -}
-                              then return (Right "")
-                              else modifyIORef (getRef ref) (\c -> c { connBffr=str })
-                                   >> readLine ref  -- recursion
-                         }
-                 | otherwise ->
-                      case elemIndex '\n' bfr of
-                          Nothing -> {- need recursion to finish line -}
-                              do { modifyIORef (getRef ref) (\c -> c { connBffr=[] })
-                                 ; more <- readLine ref -- contains extra recursion                      
-                                 ; return $ more `bindE` \str -> Right (bfr++str)
-                                 }
-                          Just i ->    {- end of line found -}
-                              let (bgn,end) = splitAt i bfr in
-                              do { modifyIORef (getRef ref) (\c -> c { connBffr=(drop 1 end) })
-                                 ; return (Right (bgn++['\n']))
-                                 }
+instance HStream Lazy.ByteString where
+    openStream       = \ a b -> openTCPConnection_ a b True
+    openSocketStream = \ a b c -> socketConnection_ a b c True
+    readBlock c n    = readBlockBS c n
+    readLine c       = readLineBS c
+    writeBlock c str = writeBlockBS c str
+    close c          = closeIt c Lazy.null True
+    closeQuick c     = closeIt c Lazy.null False
+    closeOnEnd c f   = closeEOF c f
 
+instance Stream.Stream Connection where
+  readBlock (Connection c)     = Network.TCP.readBlock c
+  readLine (Connection c)      = Network.TCP.readLine c
+  writeBlock (Connection c)    = Network.TCP.writeBlock c
+  close (Connection c)         = Network.TCP.close c
+  closeOnEnd (Connection c) f  = Network.TCP.closeEOF c f
 
+instance HStream String where
+    openStream      = openTCPConnection
+    openSocketStream = socketConnection
+    readBlock ref n = readBlockBS ref n
 
-    -- The 'Connection' object allows no outward buffering, 
+    -- This function uses a buffer, at this time the buffer is just 1000 characters.
+    -- (however many bytes this is is left to the user to decypher)
+    readLine ref = readLineBS ref
+    -- The 'Connection' object allows no outward buffering,
     -- since in general messages are serialised in their entirety.
-    writeBlock ref str =
-        readIORef (getRef ref) >>= \conn -> case conn of
-            ConnClosed -> return (Left ErrorClosed)
-            (MkConn sk addr _ _) -> fn sk addr str `Exception.catch` (handleSocketError sk)
-        where
-            fn sk addr s
-                | null s    = return (Right ())  -- done
-                | otherwise =
-                    getSocketOption sk SoError >>= \se ->
-                    if se == 0
-                        then sendTo sk s addr >>= \i -> fn sk addr (drop i s)
-                        else writeIORef (getRef ref) ConnClosed >>
-                             if se == 10054
-                                 then return (Left ErrorReset)
-                                 else return (Left $ ErrorMisc $ show se)
-
+    writeBlock ref str = writeBlockBS ref str -- (stringToBuf str)
 
     -- Closes a Connection.  Connection will no longer
     -- allow any of the other Stream functions.  Notice that a Connection may close
     -- at any time before a call to this function.  This function is idempotent.
     -- (I think the behaviour here is TCP specific)
-    close ref = 
-        do { c <- readIORef (getRef ref)
-           ; closeConn c `Exception.catch` (\_ -> return ())
-           ; writeIORef (getRef ref) ConnClosed
-           }
-        where
-            -- Be kind to peer & close gracefully.
-            closeConn (ConnClosed) = return ()
-            closeConn (MkConn sk addr [] _) =
-                do { shutdown sk ShutdownSend
-                   ; suck ref
-                   ; shutdown sk ShutdownReceive
-                   ; sClose sk
-                   }
+    close c = closeIt c null True
 
-            suck :: Connection -> IO ()
-            suck cn = readLine cn >>= 
-                      either (\_ -> return ()) -- catch errors & ignore
-                             (\x -> if null x then return () else suck cn)
+    -- Closes a Connection without munching the rest of the stream.
+    closeQuick c = closeIt c null False
 
+    closeOnEnd c f = closeEOF c f
+
+-- | @openTCPPort uri port@  establishes a connection to a remote
+-- host, using 'getHostByName' which possibly queries the DNS system, hence
+-- may trigger a network connection.
+openTCPPort :: String -> Int -> IO Connection
+openTCPPort uri port = openTCPConnection uri port >>= return.Connection
+
+-- Add a "persistent" option?  Current persistent is default.
+-- Use "Result" type for synchronous exception reporting?
+openTCPConnection :: BufferType ty => String -> Int -> IO (HandleStream ty)
+openTCPConnection uri port = openTCPConnection_ uri port False
+
+openTCPConnection_ :: BufferType ty => String -> Int -> Bool -> IO (HandleStream ty)
+openTCPConnection_ uri port stashInput = do
+    -- HACK: uri is sometimes obtained by calling Network.URI.uriRegName, and this includes
+    -- the surrounding square brackets for an RFC 2732 host like [::1]. It's not clear whether
+    -- it should, or whether all call sites should be using something different instead, but
+    -- the simplest short-term fix is to strip any surrounding square brackets here.
+    -- It shouldn't affect any as this is the only situation they can occur - see RFC 3986.
+    let fixedUri =
+         case uri of
+            '[':(rest@(c:_)) | last rest == ']'
+              -> if c == 'v' || c == 'V'
+                     then error $ "Unsupported post-IPv6 address " ++ uri
+                     else init rest
+            _ -> uri
+
+
+    -- use withSocketsDo here in case the caller hasn't used it, which would make getAddrInfo fail on Windows
+    -- although withSocketsDo is supposed to wrap the entire program, in practice it is safe to use it locally
+    -- like this as it just does a once-only installation of a shutdown handler to run at program exit,
+    -- rather than actually shutting down after the action
+    addrinfos <- withSocketsDo $ getAddrInfo (Just $ defaultHints { addrFamily = AF_UNSPEC, addrSocketType = Stream }) (Just fixedUri) (Just . show $ port)
+
+    let
+      connectAddrInfo a = bracketOnError
+        (socket (addrFamily a) Stream defaultProtocol)  -- acquire
+        Network.Socket.close                            -- release
+        ( \s -> do
+            setSocketOption s KeepAlive 1
+            connect s (addrAddress a)
+            socketConnection_ fixedUri port s stashInput )
+
+      -- try multiple addresses; return Just connected socket or Nothing
+      tryAddrInfos [] = return Nothing
+      tryAddrInfos (h:t) =
+        let next = \(_ :: IOException) -> tryAddrInfos t
+        in  try (connectAddrInfo h) >>= either next (return . Just)
+
+    case addrinfos of
+        [] -> fail "openTCPConnection: getAddrInfo returned no address information"
+
+        -- single AddrInfo; call connectAddrInfo directly so that specific
+        -- exception is thrown in event of failure
+        [ai] -> connectAddrInfo ai `catchIO` (\e -> fail $
+                  "openTCPConnection: failed to connect to "
+                  ++ show (addrAddress ai) ++ ": " ++ show e)
+
+        -- multiple AddrInfos; try each until we get a connection, or run out
+        ais ->
+          let
+            err = fail $ "openTCPConnection: failed to connect; tried addresses: "
+                         ++ show (fmap addrAddress ais)
+          in tryAddrInfos ais >>= maybe err return
+
+-- | @socketConnection@, like @openConnection@ but using a pre-existing 'Socket'.
+socketConnection :: BufferType ty
+                 => String
+                 -> Int
+                 -> Socket
+                 -> IO (HandleStream ty)
+socketConnection hst port sock = socketConnection_ hst port sock False
+
+-- Internal function used to control the on-demand streaming of input
+-- for /lazy/ streams.
+socketConnection_ :: BufferType ty
+                  => String
+                  -> Int
+                  -> Socket
+                  -> Bool
+                  -> IO (HandleStream ty)
+socketConnection_ hst port sock stashInput = do
+    h <- socketToHandle sock ReadWriteMode
+    mb <- case stashInput of { True -> liftM Just $ buf_hGetContents bufferOps h; _ -> return Nothing }
+    let conn = MkConn
+         { connSock     = sock
+         , connHandle   = h
+         , connBuffer   = bufferOps
+         , connInput    = mb
+         , connEndPoint = EndPoint hst port
+         , connHooks    = Nothing
+         , connCloseEOF = False
+         }
+    v <- newMVar conn
+    return (HandleStream v)
+
+closeConnection :: HStream a => HandleStream a -> IO Bool -> IO ()
+closeConnection ref readL = do
+    -- won't hold onto the lock for the duration
+    -- we are draining it...ToDo: have Connection
+    -- into a shutting-down state so that other
+    -- threads will simply back off if/when attempting
+    -- to also close it.
+  c <- readMVar (getRef ref)
+  closeConn c `catchIO` (\_ -> return ())
+  modifyMVar_ (getRef ref) (\ _ -> return ConnClosed)
+ where
+   -- Be kind to peer & close gracefully.
+  closeConn ConnClosed = return ()
+  closeConn conn = do
+    let sk = connSock conn
+    hFlush (connHandle conn)
+    shutdown sk ShutdownSend
+    suck readL
+    hClose (connHandle conn)
+    shutdown sk ShutdownReceive
+    Network.Socket.close sk
+
+  suck :: IO Bool -> IO ()
+  suck rd = do
+    f <- rd
+    if f then return () else suck rd
+
 -- | Checks both that the underlying Socket is connected
 -- and that the connection peer matches the given
 -- host name (which is recorded locally).
-isConnectedTo :: Connection -> String -> IO Bool
-isConnectedTo conn name =
-    do { v <- readIORef (getRef conn)
-       ; case v of
-            ConnClosed -> return False
-            (MkConn sk _ _ h) ->
-                if (map toLower h == map toLower name)
-                then sIsConnected sk
-                else return False
-       }
+isConnectedTo :: Connection -> EndPoint -> IO Bool
+isConnectedTo (Connection conn) endPoint = isTCPConnectedTo conn endPoint
+
+isTCPConnectedTo :: HandleStream ty -> EndPoint -> IO Bool
+isTCPConnectedTo conn endPoint = do
+   v <- readMVar (getRef conn)
+   case v of
+     ConnClosed -> return False
+     _
+      | connEndPoint v == endPoint ->
+          catchIO (getPeerName (connSock v) >> return True) (const $ return False)
+      | otherwise -> return False
+
+readBlockBS :: HStream a => HandleStream a -> Int -> IO (Result a)
+readBlockBS ref n = onNonClosedDo ref $ \ conn -> do
+   x <- bufferGetBlock ref n
+   maybe (return ())
+         (\ h -> hook_readBlock h (buf_toStr $ connBuffer conn) n x)
+         (connHooks' conn)
+   return x
+
+-- This function uses a buffer, at this time the buffer is just 1000 characters.
+-- (however many bytes this is is left for the user to decipher)
+readLineBS :: HStream a => HandleStream a -> IO (Result a)
+readLineBS ref = onNonClosedDo ref $ \ conn -> do
+   x <- bufferReadLine ref
+   maybe (return ())
+         (\ h -> hook_readLine h (buf_toStr $ connBuffer conn) x)
+         (connHooks' conn)
+   return x
+
+-- The 'Connection' object allows no outward buffering,
+-- since in general messages are serialised in their entirety.
+writeBlockBS :: HandleStream a -> a -> IO (Result ())
+writeBlockBS ref b = onNonClosedDo ref $ \ conn -> do
+  x    <- bufferPutBlock (connBuffer conn) (connHandle conn) b
+  maybe (return ())
+        (\ h -> hook_writeBlock h (buf_toStr $ connBuffer conn) b x)
+        (connHooks' conn)
+  return x
+
+closeIt :: HStream ty => HandleStream ty -> (ty -> Bool) -> Bool -> IO ()
+closeIt c p b = do
+   closeConnection c (if b
+                      then readLineBS c >>= \ x -> case x of { Right xs -> return (p xs); _ -> return True}
+                      else return True)
+   conn <- readMVar (getRef c)
+   maybe (return ())
+         (hook_close)
+         (connHooks' conn)
+
+closeEOF :: HandleStream ty -> Bool -> IO ()
+closeEOF c flg = modifyMVar_ (getRef c) (\ co -> return co{connCloseEOF=flg})
+
+bufferGetBlock :: HStream a => HandleStream a -> Int -> IO (Result a)
+bufferGetBlock ref n = onNonClosedDo ref $ \ conn -> do
+   case connInput conn of
+    Just c -> do
+      let (a,b) = buf_splitAt (connBuffer conn) n c
+      modifyMVar_ (getRef ref) (\ co -> return co{connInput=Just b})
+      return (return a)
+    _ -> do
+      catchIO (buf_hGet (connBuffer conn) (connHandle conn) n >>= return.return)
+              (\ e ->
+                       if isEOFError e
+                        then do
+                          when (connCloseEOF conn) $ catchIO (closeQuick ref) (\ _ -> return ())
+                          return (return (buf_empty (connBuffer conn)))
+                        else return (failMisc (show e)))
+
+bufferPutBlock :: BufferOp a -> Handle -> a -> IO (Result ())
+bufferPutBlock ops h b =
+  catchIO (buf_hPut ops h b >> hFlush h >> return (return ()))
+          (\ e -> return (failMisc (show e)))
+
+bufferReadLine :: HStream a => HandleStream a -> IO (Result a)
+bufferReadLine ref = onNonClosedDo ref $ \ conn -> do
+  case connInput conn of
+   Just c -> do
+    let (a,b0)  = buf_span (connBuffer conn) (/='\n') c
+    let (newl,b1) = buf_splitAt (connBuffer conn) 1 b0
+    modifyMVar_ (getRef ref) (\ co -> return co{connInput=Just b1})
+    return (return (buf_append (connBuffer conn) a newl))
+   _ -> catchIO
+              (buf_hGetLine (connBuffer conn) (connHandle conn) >>=
+                    return . return . appendNL (connBuffer conn))
+              (\ e ->
+                 if isEOFError e
+                  then do
+                    when (connCloseEOF conn) $ catchIO (closeQuick ref) (\ _ -> return ())
+                    return (return   (buf_empty (connBuffer conn)))
+                  else return (failMisc (show e)))
+ where
+   -- yes, this s**ks.. _may_ have to be addressed if perf
+   -- suggests worthiness.
+  appendNL ops b = buf_snoc ops b nl
+
+  nl :: Word8
+  nl = fromIntegral (fromEnum '\n')
+
+onNonClosedDo :: HandleStream a -> (Conn a -> IO (Result b)) -> IO (Result b)
+onNonClosedDo h act = do
+  x <- readMVar (getRef h)
+  case x of
+    ConnClosed{} -> return (failWith ErrorClosed)
+    _ -> act x
 
diff --git a/test/Httpd.hs b/test/Httpd.hs
new file mode 100644
--- /dev/null
+++ b/test/Httpd.hs
@@ -0,0 +1,158 @@
+{-# LANGUAGE CPP #-}
+
+module Httpd
+    ( Request, Response, Server
+    , mkResponse
+    , reqMethod, reqURI, reqHeaders, reqBody
+    , shed
+#ifdef WARP_TESTS
+    , warp
+#endif
+    )
+    where
+
+import Control.Applicative
+import Control.Arrow ( (***) )
+import Control.DeepSeq
+import Control.Monad
+import Control.Monad.Trans ( liftIO )
+import qualified Data.ByteString            as B
+import qualified Data.ByteString.Lazy       as BL
+import qualified Data.ByteString.Char8      as BC
+import qualified Data.ByteString.Lazy.Char8 as BLC
+#ifdef WARP_TESTS
+import qualified Data.CaseInsensitive       as CI
+#endif
+import Data.Maybe ( fromJust )
+import Network.URI ( URI, parseRelativeReference )
+
+import Network.Socket
+    ( getAddrInfo, AddrInfo, defaultHints, addrAddress, addrFamily
+      , addrFlags, addrSocketType, AddrInfoFlag(AI_PASSIVE), socket, Family(AF_UNSPEC,AF_INET6)
+      , defaultProtocol, SocketType(Stream), listen, setSocketOption, SocketOption(ReuseAddr)
+    )
+#ifdef WARP_TESTS
+#if MIN_VERSION_network(2,4,0)
+import Network.Socket ( bind )
+#else
+import Network.Socket ( bindSocket, Socket, SockAddr )
+#endif
+#endif
+
+import qualified Network.Shed.Httpd as Shed
+    ( Request, Response(Response), initServer
+    , reqMethod, reqURI, reqHeaders, reqBody
+    )
+#ifdef WARP_TESTS
+#if !MIN_VERSION_wai(3,0,0)
+import qualified Data.Conduit.Lazy as Warp
+#endif
+
+import qualified Network.HTTP.Types as Warp
+    ( Status(..) )
+import qualified Network.Wai as Warp
+import qualified Network.Wai.Handler.Warp as Warp
+    ( runSettingsSocket, defaultSettings, setPort )
+#endif
+
+data Request = Request
+    {
+     reqMethod :: String,
+     reqURI :: URI,
+     reqHeaders :: [(String, String)],
+     reqBody :: String
+    }
+
+data Response = Response
+    {
+     respStatus :: Int,
+     respHeaders :: [(String, String)],
+     respBody :: String
+    }
+
+mkResponse :: Int -> [(String, String)] -> String -> Response
+mkResponse = Response
+
+type Server = Int -> (Request -> IO Response) -> IO ()
+
+shed :: Server
+shed port handler =
+    () <$ Shed.initServer
+           port
+           (liftM responseToShed . handler . requestFromShed)
+  where
+     responseToShed (Response status hdrs body) =
+         Shed.Response status hdrs body
+     chomp = reverse . strip '\r' . reverse
+     strip c (c':str) | c == c' = str
+     strip c str = str
+     requestFromShed request =
+         Request
+         {
+          reqMethod = Shed.reqMethod request,
+          reqURI = Shed.reqURI request,
+          reqHeaders = map (id *** chomp) $ Shed.reqHeaders request,
+          reqBody = Shed.reqBody request
+         }
+
+#if !MIN_VERSION_bytestring(0,10,0)
+instance NFData B.ByteString where
+   rnf = rnf . B.length
+#endif
+
+#ifdef WARP_TESTS
+#if !MIN_VERSION_network(2,4,0)
+bind :: Socket -> SockAddr -> IO ()
+bind = bindSocket
+#endif
+
+warp :: Bool -> Server
+warp ipv6 port handler = do
+    addrinfos <- getAddrInfo (Just $ defaultHints { addrFamily = AF_UNSPEC, addrSocketType = Stream })
+                             (Just $ if ipv6 then "::1" else "127.0.0.1")
+                             (Just . show $ port)
+    case addrinfos of
+        [] -> fail "Couldn't obtain address information in warp"
+        (addri:_) -> do
+            sock <- socket (addrFamily addri) Stream defaultProtocol
+            setSocketOption sock ReuseAddr 1
+            bind sock (addrAddress addri)
+            listen sock 5
+#if MIN_VERSION_wai(3,0,0)
+            Warp.runSettingsSocket (Warp.setPort port Warp.defaultSettings) sock $ \warpRequest warpRespond -> do
+               request <- requestFromWarp warpRequest
+               response <- handler request
+               warpRespond (responseToWarp response)
+#else
+            Warp.runSettingsSocket (Warp.setPort port Warp.defaultSettings) sock $ \warpRequest -> do
+               request <- requestFromWarp warpRequest
+               response <- handler request
+               return (responseToWarp response)
+#endif
+  where
+     responseToWarp (Response status hdrs body) =
+         Warp.responseLBS
+                 (Warp.Status status B.empty)
+                 (map headerToWarp hdrs)
+                 (BLC.pack body)
+     headerToWarp (name, value) = (CI.mk (BC.pack name), BC.pack value)
+     headerFromWarp (name, value) =
+         (BC.unpack (CI.original name), BC.unpack value)
+     requestFromWarp request = do
+#if MIN_VERSION_wai(3,0,1)
+         body <- fmap BLC.unpack $ Warp.strictRequestBody request
+#else
+         body <- fmap BLC.unpack $ Warp.lazyRequestBody request
+         body `deepseq` return ()
+#endif
+         return $
+                Request
+                {
+                 reqMethod = BC.unpack (Warp.requestMethod request),
+                 reqURI = fromJust . parseRelativeReference .
+                          BC.unpack . Warp.rawPathInfo $
+                          request,
+                 reqHeaders = map headerFromWarp (Warp.requestHeaders request),
+                 reqBody = body
+                }
+#endif
diff --git a/test/UnitTests.hs b/test/UnitTests.hs
new file mode 100644
--- /dev/null
+++ b/test/UnitTests.hs
@@ -0,0 +1,69 @@
+module UnitTests ( unitTests ) where
+
+import Network.HTTP.Base
+import Network.HTTP.Headers
+import Network.URI
+
+import Data.Maybe ( fromJust )
+
+import Test.Framework ( testGroup )
+import Test.Framework.Providers.HUnit
+import Test.HUnit
+
+parseIPv4Address :: Assertion
+parseIPv4Address =
+    assertEqual "127.0.0.1 address is recognised"
+         (Just (URIAuthority {user = Nothing, password = Nothing, host = "127.0.0.1", port = Just 5313}))
+         (parseURIAuthority (uriToAuthorityString (fromJust (parseURI "http://127.0.0.1:5313/foo"))))
+
+
+parseIPv6Address :: Assertion
+parseIPv6Address =
+    assertEqual "::1 address"
+         (Just (URIAuthority {user = Nothing, password = Nothing, host = "::1", port = Just 5313}))
+         (parseURIAuthority (uriToAuthorityString (fromJust (parseURI "http://[::1]:5313/foo"))))
+
+customHeaderNameComparison :: Assertion
+customHeaderNameComparison =
+    assertEqual "custom header name" (HdrCustom "foo") (HdrCustom "Foo")
+
+customHeaderLookup :: Assertion
+customHeaderLookup =
+    let val = "header value"
+        h = Header (HdrCustom "foo") val
+
+    in assertEqual "custom header lookup" (Just val)
+        (lookupHeader (HdrCustom "Foo") [h])
+
+caseInsensitiveHeaderParse :: Assertion
+caseInsensitiveHeaderParse =
+    let expected = [ Header HdrContentType "blah"
+                   , Header (HdrCustom "X-Unknown") "unused"
+                   ]
+        input = [ "content-type: blah"
+                , "X-Unknown: unused"
+                ]
+
+        match actual =
+            length actual == length expected &&
+            and [ hdrName a == hdrName b && hdrValue a == hdrValue b
+                | (a, b) <- zip expected actual
+                ]
+
+    in case parseHeaders input of
+        Left _ -> assertFailure "Failed header parse"
+        Right actual -> assert (match actual)
+
+unitTests =
+    [testGroup "Unit tests"
+        [ testGroup "URI parsing"
+            [ testCase "Parse IPv4 address" parseIPv4Address
+            , testCase "Parse IPv6 address" parseIPv6Address
+            ]
+        ]
+    , testGroup "Header tests"
+        [ testCase "Custom header name case-insensitive match" customHeaderNameComparison
+        , testCase "Custom header lookup" customHeaderLookup
+        , testCase "Case-insensitive parsing" caseInsensitiveHeaderParse
+        ]
+    ]
diff --git a/test/httpTests.hs b/test/httpTests.hs
new file mode 100644
--- /dev/null
+++ b/test/httpTests.hs
@@ -0,0 +1,668 @@
+{-# LANGUAGE ImplicitParams, ViewPatterns, NoMonomorphismRestriction, CPP #-}
+import Control.Concurrent
+
+import Control.Applicative ((<$))
+import Control.Concurrent (threadDelay)
+import Control.Exception (try)
+import qualified Data.ByteString.Lazy.Char8 as BL (pack)
+import Data.Char (isSpace)
+import qualified Data.Digest.Pure.MD5 as MD5 (md5)
+import Data.List.Split (splitOn)
+import Data.Maybe (fromJust)
+import System.IO.Error (userError)
+
+import qualified Httpd
+import qualified UnitTests
+
+import Network.Browser
+import Network.HTTP
+import Network.HTTP.Base
+import Network.HTTP.Auth
+import Network.HTTP.Headers
+import Network.Stream (Result)
+import Network.URI (uriPath, parseURI)
+
+import System.Environment (getArgs)
+import System.Info (os)
+import System.IO (getChar)
+
+import Test.Framework (defaultMainWithArgs, testGroup)
+import Test.Framework.Providers.HUnit
+import Test.HUnit
+
+
+basicGetRequest :: (?testUrl :: ServerAddress) => Assertion
+basicGetRequest = do
+  response <- simpleHTTP (getRequest (?testUrl "/basic/get"))
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  assertEqual "Receiving expected response" "It works." body
+
+basicGetRequestLBS :: (?testUrl :: ServerAddress) => Assertion
+basicGetRequestLBS = do
+  response <- simpleHTTP (mkRequest GET (fromJust (parseURI (?testUrl ("/basic/get")))))
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  assertEqual "Receiving expected response" (BL.pack "It works.") body
+
+basicHeadRequest :: (?testUrl :: ServerAddress) => Assertion
+basicHeadRequest = do
+  response <- simpleHTTP (headRequest (?testUrl "/basic/head"))
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  -- the body should be empty, since this is a HEAD request
+  assertEqual "Receiving expected response" "" body
+
+basicExample :: (?testUrl :: ServerAddress) => Assertion
+basicExample = do
+  result <-
+    -- sample code from Network.HTTP haddock, with URL changed
+    -- Note there's also a copy of the example in the .cabal file
+    simpleHTTP (getRequest (?testUrl "/basic/example")) >>= fmap (take 100) . getResponseBody
+  assertEqual "Receiving expected response" (take 100 haskellOrgText) result
+
+secureGetRequest :: (?secureTestUrl :: ServerAddress) => Assertion
+secureGetRequest = do
+  response <- try $ simpleHTTP (getRequest (?secureTestUrl "/anything"))
+  assertEqual "Threw expected exception"
+              (Left (userError "https not supported"))
+              (fmap show response) -- fmap show because Response isn't in Eq
+
+basicPostRequest :: (?testUrl :: ServerAddress) => Assertion
+basicPostRequest = do
+  let sendBody = "body"
+  response <- simpleHTTP $ postRequestWithBody (?testUrl "/basic/post")
+                                               "text/plain"
+                                               sendBody
+  code <- getResponseCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+  body <- getResponseBody response
+  assertEqual "Receiving expected response"
+              (show (Just "text/plain", Just "4", sendBody))
+              body
+
+userpwAuthFailure :: (?baduserpwUrl :: ServerAddress) => Assertion
+userpwAuthFailure = do
+  response <- simpleHTTP (getRequest (?baduserpwUrl "/auth/basic"))
+  code <- getResponseCode response
+  body <- getResponseBody response
+  assertEqual "HTTP status code" ((4, 0, 1),
+                "Just \"Basic dGVzdDp3cm9uZ3B3ZA==\"") (code, body)
+  -- in case of 401, the server returns the contents of the Authz header
+
+userpwAuthSuccess :: (?userpwUrl :: ServerAddress) => Assertion
+userpwAuthSuccess = do
+  response <- simpleHTTP (getRequest (?userpwUrl "/auth/basic"))
+  code <- getResponseCode response
+  body <- getResponseBody response
+  assertEqual "Receiving expected response" ((2, 0, 0), "Here's the secret") (code, body)
+
+basicAuthFailure :: (?testUrl :: ServerAddress) => Assertion
+basicAuthFailure = do
+  response <- simpleHTTP (getRequest (?testUrl "/auth/basic"))
+  code <- getResponseCode response
+  body <- getResponseBody response
+  assertEqual "HTTP status code" ((4, 0, 1), "Nothing") (code, body)
+
+credentialsBasic :: (?testUrl :: ServerAddress) => Authority
+credentialsBasic = AuthBasic "Testing realm" "test" "password"
+                             (fromJust . parseURI . ?testUrl $ "/auth/basic")
+
+basicAuthSuccess :: (?testUrl :: ServerAddress) => Assertion
+basicAuthSuccess = do
+  let req = getRequest (?testUrl "/auth/basic")
+  let authString = withAuthority credentialsBasic req
+  let reqWithAuth = req { rqHeaders = mkHeader HdrAuthorization authString:rqHeaders req }
+  response <- simpleHTTP reqWithAuth
+  code <- getResponseCode response
+  body <- getResponseBody response
+  assertEqual "Receiving expected response" ((2, 0, 0), "Here's the secret") (code, body)
+
+utf8URLEncode :: Assertion
+utf8URLEncode = do
+  assertEqual "Normal URL" (urlEncode "what-a_mess.com") "what-a_mess.com"
+  assertEqual "Chinese URL" (urlEncode "好") "%E5%A5%BD"
+  assertEqual "Russian URL" (urlEncode "ололо") "%D0%BE%D0%BB%D0%BE%D0%BB%D0%BE"
+
+utf8URLDecode :: Assertion
+utf8URLDecode = do
+  assertEqual "Normal URL" (urlDecode "what-a_mess.com") "what-a_mess.com"
+  assertEqual "Mixed URL" (urlDecode "UTFin进入-wow") "UTFin进入-wow"
+  assertEqual "Chinese URL" (urlDecode "%E5%A5%BD") "好"
+  assertEqual "Russian URL" (urlDecode "%D0%BE%D0%BB%D0%BE%D0%BB%D0%BE") "ололо"
+
+browserExample :: (?testUrl :: ServerAddress) => Assertion
+browserExample = do
+  result <-
+    -- sample code from Network.Browser haddock, with URL changed
+    -- Note there's also a copy of the example in the .cabal file
+    do
+      (_, rsp)
+         <- Network.Browser.browse $ do
+               setAllowRedirects True -- handle HTTP redirects
+               request $ getRequest (?testUrl "/browser/example")
+      return (take 100 (rspBody rsp))
+  assertEqual "Receiving expected response" (take 100 haskellOrgText) result
+
+-- A vanilla HTTP request using Browser shouldn't send a cookie header
+browserNoCookie :: (?testUrl :: ServerAddress) => Assertion
+browserNoCookie = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    request $ getRequest (?testUrl "/browser/no-cookie")
+  let code = rspCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+
+
+-- Regression test
+--  * Browser sends vanilla request to server
+--  * Server sets one cookie "hello=world"
+--  * Browser sends a second request
+--
+-- Expected: Server gets single cookie with "hello=world"
+-- Actual:   Server gets 3 extra cookies, which are actually cookie attributes:
+--           "$Version=0;hello=world;$Domain=localhost:8080\r"
+browserOneCookie :: (?testUrl :: ServerAddress) => Assertion
+browserOneCookie = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    -- This first requests returns a single Set-Cookie: hello=world
+    _ <- request $ getRequest (?testUrl "/browser/one-cookie/1")
+
+    -- This second request should send a single Cookie: hello=world
+    request $ getRequest (?testUrl "/browser/one-cookie/2")
+  let body = rspBody response
+  assertEqual "Receiving expected response" "" body
+  let code = rspCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+
+browserTwoCookies :: (?testUrl :: ServerAddress) => Assertion
+browserTwoCookies = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    -- This first request returns two cookies
+    _ <- request $ getRequest (?testUrl "/browser/two-cookies/1")
+
+    -- This second request should send them back
+    request $ getRequest (?testUrl "/browser/two-cookies/2")
+  let body = rspBody response
+  assertEqual "Receiving expected response" "" body
+  let code = rspCode response
+  assertEqual "HTTP status code" (2, 0, 0) code
+
+
+browserFollowsRedirect :: (?testUrl :: ServerAddress) => Int -> Assertion
+browserFollowsRedirect n = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    request $ getRequest (?testUrl "/browser/redirect/relative/" ++ show n ++ "/basic/get")
+  assertEqual "Receiving expected response from server"
+              ((2, 0, 0), "It works.")
+              (rspCode response, rspBody response)
+
+browserReturnsRedirect :: (?testUrl :: ServerAddress) => Int -> Assertion
+browserReturnsRedirect n = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+    request $ getRequest (?testUrl "/browser/redirect/relative/" ++ show n ++ "/basic/get")
+  assertEqual "Receiving expected response from server"
+              ((n `div` 100, n `mod` 100 `div` 10, n `mod` 10), "")
+              (rspCode response, rspBody response)
+
+authGenBasic _ "Testing realm" = return $ Just ("test", "password")
+authGenBasic _ realm = fail $ "Unexpected realm " ++ realm
+
+browserBasicAuth :: (?testUrl :: ServerAddress) => Assertion
+browserBasicAuth = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+
+    setAuthorityGen authGenBasic
+
+    request $ getRequest (?testUrl "/auth/basic")
+
+  assertEqual "Receiving expected response from server"
+              ((2, 0, 0), "Here's the secret")
+              (rspCode response, rspBody response)
+
+authGenDigest _ "Digest testing realm" = return $ Just ("test", "digestpassword")
+authGenDigest _ realm = fail $ "Unexpected digest realm " ++ realm
+
+browserDigestAuth :: (?testUrl :: ServerAddress) => Assertion
+browserDigestAuth = do
+  (_, response) <- browse $ do
+    setOutHandler (const $ return ())
+
+    setAuthorityGen authGenDigest
+
+    request $ getRequest (?testUrl "/auth/digest")
+
+  assertEqual "Receiving expected response from server"
+              ((2, 0, 0), "Here's the digest secret")
+              (rspCode response, rspBody response)
+
+
+
+browserAlt :: (?altTestUrl :: ServerAddress) => Assertion
+browserAlt = do
+  (response) <- browse $ do
+
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?altTestUrl "/basic/get")
+
+    return response1
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response, rspBody response)
+
+-- test that requests to multiple servers on the same host
+-- don't get confused with each other
+browserBoth :: (?testUrl :: ServerAddress, ?altTestUrl :: ServerAddress) => Assertion
+browserBoth = do
+  (response1, response2) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response2, rspBody response2)
+
+-- test that requests to multiple servers on the same host
+-- don't get confused with each other
+browserBothReversed :: (?testUrl :: ServerAddress, ?altTestUrl :: ServerAddress) => Assertion
+browserBothReversed = do
+  (response1, response2) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get")
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response2, rspBody response2)
+
+browserSecureRequest :: (?secureTestUrl :: ServerAddress) => Assertion
+browserSecureRequest = do
+  res <- try $ browse $ do
+    setOutHandler (const $ return ())
+
+    request $ getRequest (?secureTestUrl "/anything")
+
+  assertEqual "Threw expected exception"
+              (Left (userError "https not supported"))
+              (fmap show res) -- fmap show because Response isn't in Eq
+
+-- in case it tries to reuse the connection
+browserSecureRequestAfterInsecure :: (?testUrl :: ServerAddress, ?secureTestUrl :: ServerAddress) => Assertion
+browserSecureRequestAfterInsecure = do
+  res <- try $ browse $ do
+    setOutHandler (const $ return ())
+
+    request $ getRequest (?testUrl "/basic/get")
+    request $ getRequest (?secureTestUrl "/anything")
+
+  assertEqual "Threw expected exception"
+              (Left (userError "https not supported"))
+              (fmap show res) -- fmap show because Response isn't in Eq
+
+browserRedirectToSecure :: (?testUrl :: ServerAddress, ?secureTestUrl :: ServerAddress) => Assertion
+browserRedirectToSecure = do
+  res <- try $ browse $ do
+    setOutHandler (const $ return ())
+    setErrHandler fail
+
+    request $ getRequest (?testUrl "/browser/redirect/secure/301/anything")
+
+  assertEqual "Threw expected exception"
+              (Left (userError $ "Unable to handle redirect, unsupported scheme: " ++ ?secureTestUrl "/anything"))
+              (fmap show res) -- fmap show because Response isn't in Eq
+
+browserTwoRequests :: (?testUrl :: ServerAddress) => Assertion
+browserTwoRequests = do
+  (response1, response2) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?testUrl "/basic/get2")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works (2).")
+              (rspCode response2, rspBody response2)
+
+
+browserTwoRequestsAlt :: (?altTestUrl :: ServerAddress) => Assertion
+browserTwoRequestsAlt = do
+  (response1, response2) <- browse $ do
+
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?altTestUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get2")
+
+    return (response1, response2)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server (2).")
+              (rspCode response2, rspBody response2)
+
+browserTwoRequestsBoth :: (?testUrl :: ServerAddress, ?altTestUrl :: ServerAddress) => Assertion
+browserTwoRequestsBoth = do
+  (response1, response2, response3, response4) <- browse $ do
+    setOutHandler (const $ return ())
+
+    (_, response1) <- request $ getRequest (?testUrl "/basic/get")
+    (_, response2) <- request $ getRequest (?altTestUrl "/basic/get")
+    (_, response3) <- request $ getRequest (?testUrl "/basic/get2")
+    (_, response4) <- request $ getRequest (?altTestUrl "/basic/get2")
+
+    return (response1, response2, response3, response4)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works.")
+              (rspCode response1, rspBody response1)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server.")
+              (rspCode response2, rspBody response2)
+
+  assertEqual "Receiving expected response from main server"
+              ((2, 0, 0), "It works (2).")
+              (rspCode response3, rspBody response3)
+
+  assertEqual "Receiving expected response from alternate server"
+              ((2, 0, 0), "This is the alternate server (2).")
+              (rspCode response4, rspBody response4)
+
+hasPrefix :: String -> String -> Maybe String
+hasPrefix [] ys = Just ys
+hasPrefix (x:xs) (y:ys) | x == y = hasPrefix xs ys
+hasPrefix _ _ = Nothing
+
+maybeRead :: Read a => String -> Maybe a
+maybeRead s =
+   case reads s of
+     [(v, "")] -> Just v
+     _ -> Nothing
+
+splitFields = map (toPair '=' . trim isSpace) . splitOn ","
+
+toPair c str = case break (==c) str of
+                 (left, _:right) -> (left, right)
+                 _ -> error $ "No " ++ show c ++ " in " ++ str
+trim f = dropWhile f . reverse . dropWhile f . reverse
+
+isSubsetOf xs ys = all (`elem` ys) xs
+
+-- first bits of result text from haskell.org (just to give some representative text)
+haskellOrgText =
+  "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\
+\\t<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\" dir=\"ltr\">\
+\\t<head>\
+\\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\
+\\t\t\t\t<meta name=\"keywords\" content=\"Haskell,Applications and libraries,Books,Foreign Function Interface,Functional programming,Hac Boston,HakkuTaikai,HaskellImplementorsWorkshop/2011,Haskell Communities and Activities Report,Haskell in education,Haskell in industry\" />"
+
+digestMatch
+  username realm password
+  nonce opaque
+  method relativeURI makeAbsolute
+  headers
+  =
+  common `isSubsetOf` headers && (relative `isSubsetOf` headers || absolute `isSubsetOf` headers)
+ where
+   common = [("username", show username), ("realm", show realm), ("nonce", show nonce),
+             ("opaque", show opaque)]
+   md5 = show . MD5.md5 . BL.pack
+   ha1 = md5 (username++":"++realm++":"++password)
+   ha2 uri = md5 (method++":"++uri)
+   response uri = md5 (ha1 ++ ":" ++ nonce ++ ":" ++ ha2 uri)
+   mkUncommon uri hash = [("uri", show uri), ("response", show hash)]
+   relative = mkUncommon relativeURI (response relativeURI)
+   absoluteURI = makeAbsolute relativeURI
+   absolute = mkUncommon absoluteURI (response absoluteURI)
+
+processRequest :: (?testUrl :: ServerAddress, ?secureTestUrl :: ServerAddress)
+               => Httpd.Request
+               -> IO Httpd.Response
+processRequest req = do
+  case (Httpd.reqMethod req, Network.URI.uriPath (Httpd.reqURI req)) of
+    ("GET", "/basic/get") -> return $ Httpd.mkResponse 200 [] "It works."
+    ("GET", "/basic/get2") -> return $ Httpd.mkResponse 200 [] "It works (2)."
+    ("GET", "/basic/head") -> return $ Httpd.mkResponse 200 [] "Body for /basic/head."
+    ("HEAD", "/basic/head") -> return $ Httpd.mkResponse 200 [] "Body for /basic/head."
+    ("POST", "/basic/post") ->
+        let typ = lookup "Content-Type" (Httpd.reqHeaders req)
+            len = lookup "Content-Length" (Httpd.reqHeaders req)
+            body = Httpd.reqBody req
+        in return $ Httpd.mkResponse 200 [] (show (typ, len, body))
+
+    ("GET", "/basic/example") ->
+      return $ Httpd.mkResponse 200 [] haskellOrgText
+
+    ("GET", "/auth/basic") ->
+      case lookup "Authorization" (Httpd.reqHeaders req) of
+        Just "Basic dGVzdDpwYXNzd29yZA==" -> return $ Httpd.mkResponse 200 [] "Here's the secret"
+        x -> return $ Httpd.mkResponse 401 [("WWW-Authenticate", "Basic realm=\"Testing realm\"")] (show x)
+
+    ("GET", "/auth/digest") ->
+      case lookup "Authorization" (Httpd.reqHeaders req) of
+        Just (hasPrefix "Digest " -> Just (splitFields -> items))
+          | digestMatch "test" "Digest testing realm" "digestpassword"
+                        "87e4" "057d"
+                        "GET" "/auth/digest" ?testUrl
+                        items
+          -> return $ Httpd.mkResponse 200 [] "Here's the digest secret"
+        x -> return $ Httpd.mkResponse
+                        401
+                        [("WWW-Authenticate",
+                          "Digest realm=\"Digest testing realm\", opaque=\"057d\", nonce=\"87e4\"")]
+                        (show x)
+
+    ("GET", "/browser/example") ->
+      return $ Httpd.mkResponse 200 [] haskellOrgText
+    ("GET", "/browser/no-cookie") ->
+      case lookup "Cookie" (Httpd.reqHeaders req) of
+        Nothing -> return $ Httpd.mkResponse 200 [] ""
+        Just s  -> return $ Httpd.mkResponse 500 [] s
+    ("GET", "/browser/one-cookie/1") ->
+      return $ Httpd.mkResponse 200 [("Set-Cookie", "hello=world")] ""
+    ("GET", "/browser/one-cookie/2") ->
+      case lookup "Cookie" (Httpd.reqHeaders req) of
+        Just "hello=world" -> return $ Httpd.mkResponse 200 [] ""
+        Just s               -> return $ Httpd.mkResponse 500 [] s
+        Nothing              -> return $ Httpd.mkResponse 500 [] (show $ Httpd.reqHeaders req)
+    ("GET", "/browser/two-cookies/1") ->
+      return $ Httpd.mkResponse 200
+                              [("Set-Cookie", "hello=world")
+                              ,("Set-Cookie", "goodbye=cruelworld")]
+                              ""
+    ("GET", "/browser/two-cookies/2") ->
+      case lookup "Cookie" (Httpd.reqHeaders req) of
+        -- TODO generalise the cookie parsing to allow for whitespace/ordering variations
+        Just "goodbye=cruelworld; hello=world" -> return $ Httpd.mkResponse 200 [] ""
+        Just s               -> return $ Httpd.mkResponse 500 [] s
+        Nothing              -> return $ Httpd.mkResponse 500 [] (show $ Httpd.reqHeaders req)
+    ("GET", hasPrefix "/browser/redirect/relative/" -> Just (break (=='/') -> (maybeRead -> Just n, rest))) ->
+      return $ Httpd.mkResponse n [("Location", rest)] ""
+    ("GET", hasPrefix "/browser/redirect/absolute/" -> Just (break (=='/') -> (maybeRead -> Just n, rest))) ->
+      return $ Httpd.mkResponse n [("Location", ?testUrl rest)] ""
+    ("GET", hasPrefix "/browser/redirect/secure/" -> Just (break (=='/') -> (maybeRead -> Just n, rest))) ->
+      return $ Httpd.mkResponse n [("Location", ?secureTestUrl rest)] ""
+    _                     -> return $ Httpd.mkResponse 500 [] "Unknown request"
+
+altProcessRequest :: Httpd.Request -> IO Httpd.Response
+altProcessRequest req = do
+  case (Httpd.reqMethod req, Network.URI.uriPath (Httpd.reqURI req)) of
+    ("GET", "/basic/get") -> return $ Httpd.mkResponse 200 [] "This is the alternate server."
+    ("GET", "/basic/get2") -> return $ Httpd.mkResponse 200 [] "This is the alternate server (2)."
+    _                     -> return $ Httpd.mkResponse 500 [] "Unknown request"
+
+maybeTestGroup True name xs = testGroup name xs
+maybeTestGroup False name _ = testGroup name []
+
+basicTests =
+    testGroup "Basic tests"
+    [ testCase "Basic GET request" basicGetRequest
+    , testCase "Basic GET request (lazy bytestring)" basicGetRequestLBS
+    , testCase "Network.HTTP example code" basicExample
+    , testCase "Secure GET request" secureGetRequest
+    , testCase "Basic POST request" basicPostRequest
+    , testCase "Basic HEAD request" basicHeadRequest
+    , testCase "URI user:pass Auth failure" userpwAuthFailure
+    , testCase "URI user:pass Auth success" userpwAuthSuccess
+    , testCase "Basic Auth failure" basicAuthFailure
+    , testCase "Basic Auth success" basicAuthSuccess
+    , testCase "UTF-8 urlEncode" utf8URLEncode
+    , testCase "UTF-8 urlDecode" utf8URLDecode
+    ]
+
+browserTests =
+    testGroup "Browser tests"
+    [ testGroup "Basic"
+      [
+        testCase "Network.Browser example code" browserExample
+      , testCase "Two requests" browserTwoRequests
+      ]
+    , testGroup "Secure"
+      [
+        testCase "Secure request" browserSecureRequest
+      , testCase "After insecure" browserSecureRequestAfterInsecure
+      , testCase "Redirection" browserRedirectToSecure
+      ]
+    , testGroup "Cookies"
+      [ testCase "No cookie header" browserNoCookie
+      , testCase "One cookie" browserOneCookie
+      , testCase "Two cookies" browserTwoCookies
+      ]
+    , testGroup "Redirection"
+      [ -- See http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection
+        -- 300 Multiple Choices: client has to handle this
+        testCase "300" (browserReturnsRedirect 300)
+        -- 301 Moved Permanently: should follow
+      , testCase "301" (browserFollowsRedirect 301)
+        -- 302 Found: should follow
+      , testCase "302" (browserFollowsRedirect 302)
+        -- 303 See Other: should follow (directly for GETs)
+      , testCase "303" (browserFollowsRedirect 303)
+        -- 304 Not Modified: maybe Browser could do something intelligent based on
+        -- being given locally cached content and sending If-Modified-Since, but it
+        -- doesn't at the moment
+      , testCase "304" (browserReturnsRedirect 304)
+      -- 305 Use Proxy: test harness doesn't have a proxy (yet)
+      -- 306 Switch Proxy: obsolete
+      -- 307 Temporary Redirect: should follow
+      , testCase "307" (browserFollowsRedirect 307)
+      -- 308 Resume Incomplete: no support for Resumable HTTP so client has to handle this
+      , testCase "308" (browserReturnsRedirect 308)
+      ]
+    , testGroup "Authentication"
+      [ testCase "Basic" browserBasicAuth
+      , testCase "Digest" browserDigestAuth
+      ]
+    ]
+
+port80Tests =
+    testGroup "Multiple servers"
+    [ testCase "Alternate server" browserAlt
+    , testCase "Both servers" browserBoth
+    , testCase "Both servers (reversed)" browserBothReversed
+    , testCase "Two requests - alternate server" browserTwoRequestsAlt
+    , testCase "Two requests - both servers" browserTwoRequestsBoth
+    ]
+
+data InetFamily = IPv4 | IPv6
+
+familyToLocalhost :: InetFamily -> String
+familyToLocalhost IPv4 = "127.0.0.1"
+familyToLocalhost IPv6 = "[::1]"
+
+urlRoot :: InetFamily -> String -> Int -> String
+urlRoot fam userpw 80 = "http://" ++ userpw ++ familyToLocalhost fam
+urlRoot fam userpw n = "http://" ++ userpw ++ familyToLocalhost fam ++ ":" ++ show n
+
+secureRoot :: InetFamily -> String -> Int -> String
+secureRoot fam userpw 443 = "https://" ++ userpw ++ familyToLocalhost fam
+secureRoot fam userpw n = "https://" ++ userpw ++ familyToLocalhost fam ++ ":" ++ show n
+
+type ServerAddress = String -> String
+
+httpAddress, httpsAddress :: InetFamily -> String -> Int -> ServerAddress
+httpAddress fam userpw port p = urlRoot fam userpw port ++ p
+httpsAddress fam userpw port p = secureRoot fam userpw port ++ p
+
+main :: IO ()
+main = do
+  args <- getArgs
+
+  let servers =
+          [ ("httpd-shed", Httpd.shed, IPv4)
+#ifdef WARP_TESTS
+          , ("warp.v6", Httpd.warp True, IPv6)
+          , ("warp.v4", Httpd.warp False, IPv4)
+#endif
+          ]
+      basePortNum, altPortNum :: Int
+      basePortNum = 5812
+      altPortNum = 80
+      numberedServers = zip [basePortNum..] servers
+
+  let setupNormalTests = do
+      flip mapM numberedServers $ \(portNum, (serverName, server, family)) -> do
+         let ?testUrl = httpAddress family "" portNum
+             ?userpwUrl = httpAddress family "test:password@" portNum
+             ?baduserpwUrl = httpAddress family "test:wrongpwd@" portNum
+             ?secureTestUrl = httpsAddress family "" portNum
+         _ <- forkIO $ server portNum processRequest
+         return $ testGroup serverName [basicTests, browserTests]
+
+  let setupAltTests = do
+      let (portNum, (_, server,family)) = head numberedServers
+      let ?testUrl = httpAddress family "" portNum
+          ?altTestUrl = httpAddress family "" altPortNum
+      _ <- forkIO $ server altPortNum altProcessRequest
+      return port80Tests
+
+  case args of
+     ["server"] -> do -- run only the harness servers for diagnostic/debug purposes
+                      -- halt on any keypress
+        _ <- setupNormalTests
+        _ <- setupAltTests
+        _ <- getChar
+        return ()
+     ("--withport80":args) -> do
+        normalTests <- setupNormalTests
+        altTests <- setupAltTests
+        _ <- threadDelay 1000000 -- Give the server time to start :-(
+        defaultMainWithArgs (UnitTests.unitTests ++ normalTests ++ [altTests]) args
+     args -> do -- run the test harness as normal
+        normalTests <- setupNormalTests
+        _ <- threadDelay 1000000 -- Give the server time to start :-(
+        defaultMainWithArgs (UnitTests.unitTests ++ normalTests) args
