packages feed

http-streams 0.6.1.1 → 0.8.9.9

raw patch · 34 files changed

Files

+ CHANGELOG.md view
@@ -0,0 +1,37 @@+* _v0.8.9_+    Ongoing maintenance continues, including support for GHC 9.2 and GHC 9.4++* _v0.8.6_  +	Internal modules are exposed. Mostly so the test suite would only+	depend on the library and not the code directly, but occasionally+	someone needed to poke at the internals. Usual warning against+	doing that applies. They are _not_ visible in the generated Haddock+	documentation.++* _v0.8.4_  +	Support GHC 8.0++* _v0.8.3_  +	A pure version of `buildRequest` is now available as `buildRequest1`.+	Support for connecting to Unix domain sockets has been added.++* _v0.7.0_  +	The Request, Response, Headers, and RequestBuilder types have been+	factored out and moved to **http-common**. They are still exported+	by **http-streams**.++* _v0.6.0_  +	Entity body lengths (both for Requests and Responses) now Int64.+	Library depends on **io-streams** 1.1.++* _v0.5.0_  +	Definition of Hostname and Port have been changed to ByteString+	and Word16, respectively.++* _v0.4.0_  +	Type signature of `buildRequest` changed, removing the Connection+	parameter. This allows you to construct Request objects before+	opening a connection to the web server if you wish.++* _v0.3.1_  +	Initial public release
− LICENCE
@@ -1,32 +0,0 @@-An HTTP client for use with io-streams--Copyright © 2012 Operational Dynamics Consulting, Pty Ltd-All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions-are met:--    1. Redistributions of source code must retain the above copyright-       notice, this list of conditions and the following disclaimer.--    2. Redistributions in binary form must reproduce the above-       copyright notice, this list of conditions and the following-       disclaimer in the documentation and/or other materials provided-       with the distribution.-      -    3. Neither the name of the project nor the names of its contributors-       may be used to endorse or promote products derived from this -       software without specific prior written permission.--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright © 2012-2023 Athae Eredh Siniath and Others+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++    1. Redistributions of source code must retain the above copyright+       notice, this list of conditions and the following disclaimer.++    2. Redistributions in binary form must reproduce the above+       copyright notice, this list of conditions and the following+       disclaimer in the documentation and/or other materials provided+       with the distribution.+      +    3. Neither the name of the project nor the names of its contributors+       may be used to endorse or promote products derived from this +       software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,57 @@+An HTTP client+==============++An HTTP client library for Haskell using the Snap Framework's+[io-streams](https://hackage.haskell.org/package/io-streams) library to handle+the streaming IO.++A common case in writing RESTful web services is needing to make onward calls+to further servers. This package is intended to make this easy to do.+Though originally written for making calls from web apps written with+Snap, you can use this from any library or framework.++Enjoy!++Example+-------++The underlying API is very simple:++```haskell+main :: IO ()+main = do+    c <- openConnection "www.example.com" 80+    +    let q = buildRequest1 $ do+                http GET "/"+                setAccept "text/html"+    +    sendRequest c q emptyBody+    +    receiveResponse c (\p i -> do+    	putStr $ show p++    	x <- Streams.read i+    	S.putStr $ fromMaybe "" x)+    +    closeConnection c+```++There are also convenience functions for the common case of making+straight-forward GET and POST requests; for instance:++```haskell+    get "http://www.example.com/" (\_ i -> Streams.connect i stdout)+```++will _{ahem}_ stream the response body to stdout. Perhaps more+interesting (though less streams-oriented), is simply getting the+response as a ByteString using one of the pre-defined handlers:++```haskell+    x' <- get "https://secure.example.com/" concatHandler+```++See the documentation in+[Network.Http.Client](https://hackage.haskell.org/package/http-streams/docs/Network-Http-Client.html)+for further examples and details of usage of the API.
Setup.hs view
@@ -1,64 +1,2 @@------ HTTP client for use with io-streams------ Copyright © 2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.-----import Data.Char (toUpper)-import Distribution.Text (display)-import Distribution.PackageDescription (PackageDescription(..)) import Distribution.Simple-import Distribution.Simple.LocalBuildInfo (LocalBuildInfo)-import Distribution.Simple.Setup (ConfigFlags)-import Distribution.System (OS (..), buildOS)-import System.IO (IOMode (..), Handle, hPutStrLn, withFile)--main :: IO ()-main = defaultMainWithHooks $ simpleUserHooks {-       postConf = configure-    }--{--    Simple detection of which operating system we're building on;-    there's no need to link the Cabal logic into our library, so-    we'll keep using CPP in Network.Http.Inconvenience.--}--configure :: Args -> ConfigFlags -> PackageDescription -> LocalBuildInfo -> IO ()-configure _ _ p _  = do--    withFile "config.h" WriteMode (\h -> do-        discoverOperatingSystem h-        discoverLibraryVersion h p)--    return ()--discoverOperatingSystem :: Handle -> IO ()-discoverOperatingSystem h = do-        hPutStrLn h ("#define " ++ s)--  where-    o = buildOS--    s = case o of-            Linux   -> "__LINUX__"-            OSX     -> "__MACOSX__"-            Windows -> "__WINDOWS__"-            _       -> "__" ++ up o ++ "__"--    up x = map toUpper (show x)--discoverLibraryVersion :: Handle -> PackageDescription -> IO ()-discoverLibraryVersion h p = do-        hPutStrLn h ("#define VERSION \"http-streams/" ++ s ++ "\"")--  where-    i = package p-    v = pkgVersion i-    s = display v-+main = defaultMain
http-streams.cabal view
@@ -1,12 +1,10 @@-cabal-version:       >= 1.10+cabal-version:       1.24 name:                http-streams-version:             0.6.1.1+version:             0.8.9.9 synopsis:            An HTTP client using io-streams description:- /Overview/- .  An HTTP client, using the Snap Framework's 'io-streams' library to- hande the streaming IO. The API is optimized for ease of use for the+ handle the streaming IO. The API is optimized for ease of use for the  rather common case of code needing to query web services and deal with  the result.  .@@ -14,48 +12,71 @@  for full documentation.  license:             BSD3-license-file:        LICENCE-author:              Andrew Cowie <andrew@operationaldynamics.com>-maintainer:          Andrew Cowie <andrew@operationaldynamics.com>-copyright:           © 2012-2013 Operational Dynamics Consulting, Pty Ltd and Others-category:            Web-tested-with:         GHC == 7.6+license-file:        LICENSE+author:              Andrew Cowie <istathar@gmail.com>+maintainer:          Andrew Cowie <istathar@gmail.com>+copyright:           © 2012-2023 Athae Eredh Siniath and Others+category:            Web, IO-Streams+tested-with:         GHC == 8.10.7, GHC == 9.2.7, GHC == 9.4.5 stability:           experimental-homepage:            http://research.operationaldynamics.com/projects/http-streams/-bug-reports:         https://github.com/afcowie/http-streams/issues+homepage:            https://github.com/aesiniath/http-streams/+bug-reports:         https://github.com/aesiniath/http-streams/issues+extra-source-files:  README.md CHANGELOG.md+                     tests/MockServer.hs+                     tests/TestSuite.hs+                     tests/data-eu-gdp.json+                     tests/data-us-gdp.json+                     tests/data-jp-gdp.json+                     tests/statler.jpg+                     tests/multipart.bin+                     tests/example1.txt+                     tests/example2.txt+                     tests/example3.txt+                     tests/example4.txt+                     tests/example5.txt+                     tests/hello.txt+                     tests/hello.html -build-type:          Custom+build-type:          Simple +flag network-uri+   description: Get Network.URI from the network-uri package+   default: True+ library   default-language:  Haskell2010    build-depends:     attoparsec,-                     base >= 4 && <5,+                     base >= 4.5 && <5,+                     directory,                      base64-bytestring,-                     blaze-builder,+                     blaze-builder >= 0.4,                      bytestring,                      case-insensitive,-                     io-streams >= 1.1 && <1.2,-                     HsOpenSSL,-                     openssl-streams >= 1.1 && <1.2,+                     filepath,+                     io-streams >= 1.3 && < 1.6,+                     HsOpenSSL >= 0.11.2,+                     openssl-streams >= 1.1 && < 1.4,                      mtl,                      transformers,-                     network,                      text,                      unordered-containers,-                     aeson+                     aeson < 2.3,+                     attoparsec-aeson < 2.3,+                     http-common >= 0.8.3.4+  if flag(network-uri)+    build-depends: network-uri >= 2.6, network >= 2.6+  else+    build-depends: network-uri < 2.6, network < 2.6 -  hs-source-dirs:    src-  exposed-modules:   Network.Http.Client-  other-modules:     Network.Http.Types,+  hs-source-dirs:    lib+  exposed-modules:   Network.Http.Client,                      Network.Http.Connection,-                     Network.Http.RequestBuilder,                      Network.Http.ResponseParser,-                     Network.Http.Utilities,                      Network.Http.Inconvenience+  other-modules:     Network.Http.Utilities -  ghc-options:       -O2-                     -Wall+  ghc-options:       -Wall                      -Wwarn                      -fwarn-tabs                      -funbox-strict-fields@@ -65,7 +86,7 @@    include-dirs:      . -  ghc-prof-options:  -prof -fprof-auto-top+  ghc-prof-options:  -fprof-auto-exported   test-suite           check@@ -76,10 +97,11 @@   build-depends:                      HUnit,                      HsOpenSSL,-                     MonadCatchIO-transformers,+                     lifted-base,                      aeson-pretty,                      attoparsec,                      base,+                     directory,                      blaze-builder,                      base64-bytestring,                      bytestring,@@ -90,21 +112,24 @@                      io-streams,                      mtl,                      transformers,-                     network,-                     openssl-streams,-                     snap-core       >= 0.9    && < 1.0,-                     snap-server     >= 0.9    && < 1.0,-                     system-fileio   >= 0.3.10 && < 0.4,-                     system-filepath >= 0.4.1  && < 0.5,+                     network >= 2.6,+                     network-uri >= 2.6,+                     openssl-streams >= 1.1    && < 1.4,+                     random,+                     snap-core       >= 1.0    && < 1.2,+                     snap-server     >= 1.1    && < 1.2,                      text,                      unordered-containers,-                     aeson+                     aeson,+                     http-common  >= 0.8.2,+                     http-streams -  hs-source-dirs:    src,tests-  main-is:           Check.hs+  hs-source-dirs:    tests+  main-is:           check.hs+  other-modules:     TestSuite+                     MockServer -  ghc-options:       -O2-                     -threaded+  ghc-options:       -threaded                      -Wall                      -Wwarn                      -fwarn-tabs@@ -118,7 +143,7 @@  source-repository    head   type:              git-  location:          git://github.com/afcowie/http-streams.git+  location:          git://github.com/aesiniath/http-streams.git   -- vim: set tabstop=21 expandtab:
+ lib/Network/Http/Client.hs view
@@ -0,0 +1,202 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2021 Athae Eredh Siniath and Others+--+-- The code in this file, and the program it is a part of, is+-- made available to you by its authors as open source software:+-- you can redistribute it and/or modify it under the terms of+-- the BSD licence.+--++{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS -fno-warn-orphans #-}++-- |+-- Maintainer: Andrew Cowie+-- Stability: Experimental+--+-- /Overview/+--+-- A simple HTTP client library, using the Snap Framework's @io-streams@+-- library to handle the streaming I\/O. The @http-streams@ API is designed+-- for ease of use when querying web services and dealing with the result.+--+-- Given:+--+-- > {-# LANGUAGE OverloadedStrings #-}+-- >+-- > import System.IO.Streams (InputStream, OutputStream, stdout)+-- > import qualified System.IO.Streams as Streams+-- > import qualified Data.ByteString as S+--+-- and this library:+--+-- > import Network.Http.Client+--+-- the underlying API is straight-forward. In particular, constructing the+-- 'Request' to send is quick and to the point:+--+-- @+-- main :: IO ()+-- main = do+-- \    c <- 'openConnection' \"www.example.com\" 80+--+-- \    let q = 'buildRequest1' $ do+--                 'http' GET \"\/\"+--                 'setAccept' \"text/html\"+--+-- \    'sendRequest' c q 'emptyBody'+--+-- \    `receiveResponse` c (\\p i -> do+--         xm <- Streams.read i+--         case xm of+--             Just x    -> S.putStr x+--             Nothing   -> \"\")+--+-- \    'closeConnection' c+-- @+--+-- which would print the first chunk of the response back from the+-- server. Obviously in real usage you'll do something more interesting+-- with the 'Response' in the handler function, and consume the entire+-- response body from the InputStream ByteString.+--+-- Because this is all happening in 'IO' (the defining feature of+-- @io-streams@!), you can ensure resource cleanup on normal or+-- abnormal termination by using @Control.Exception@'s standard+-- 'Control.Exception.bracket' function; see 'closeConnection' for an+-- example. For the common case we have a utility function which+-- wraps @bracket@ for you:+--+-- @+-- foo :: IO ByteString+-- foo = 'withConnection' ('openConnection' \"www.example.com\" 80) doStuff+--+-- doStuff :: Connection -> IO ByteString+-- @+--+-- There are also a set of convenience APIs that do just that, along with+-- the tedious bits like parsing URLs. For example, to do an HTTP GET and+-- stream the response body to stdout, you can simply do:+--+-- @+--     'get' \"http:\/\/www.example.com\/file.txt\" (\\p i -> Streams.connect i stdout)+-- @+--+-- which on the one hand is \"easy\" while on the other exposes the the+-- 'Response' and InputStream for you to read from. Of course, messing+-- around with URLs is all a bit inefficient, so if you already have e.g.+-- hostname and path, or if you need more control over the request being+-- created, then the underlying @http-streams@ API is simple enough to use+-- directly.+--+module Network.Http.Client (+    -- * Connecting to server+    Hostname,+    Port,+    Connection,+    openConnection,+    openConnectionUnix,++    -- * Building Requests+    -- | You setup a request using the RequestBuilder monad, and+    -- get the resultant Request object by running 'buildRequest1'. The+    -- first call doesn't have to be to 'http', but it looks better when+    -- it is, don't you think?+    Method(..),+    RequestBuilder,+    buildRequest1,+    buildRequest,+    http,+    setHostname,+    setAccept,+    setAccept',+    setAuthorizationBasic,+    ContentType,+    setContentType,+    setContentLength,+    FieldName,+    Boundary,+    randomBoundary,+    setContentMultipart,+    setExpectContinue,+    setTransferEncoding,+    setHeader,++    -- * Sending HTTP request+    Request,+    Response,+    getHostname,+    sendRequest,+    emptyBody,+    simpleBody,+    fileBody,+    inputStreamBody,+    encodedFormBody,+    multipartFormBody,+    Part,+    simplePart,+    filePart,+    inputStreamPart,+    jsonBody,++    -- * Processing HTTP response+    receiveResponse,+    receiveResponseRaw,+    unsafeReceiveResponse,+    UnexpectedCompression,+    StatusCode,+    getStatusCode,+    getStatusMessage,+    getHeader,+    debugHandler,+    simpleHandler,+    simpleHandler',+    HttpClientError(..),+    jsonHandler,++    -- * Resource cleanup+    closeConnection,+    withConnection,++    -- * Convenience APIs+    -- | Some simple functions for making requests with useful defaults.+    -- There's no @head@ function for the usual reason of needing to+    -- avoid collision with @Prelude@.+    --+    -- These convenience functions work with @http@ and @https@, but+    --  note that if you retrieve an @https@ URL, you /must/ wrap your+    -- @main@ function with 'OpenSSL.withOpenSSL' to initialize the+    -- native openssl library code.+    --+    URL,+    get,+    TooManyRedirects,+    post,+    postForm,+    put,++    -- * Secure connections+    openConnectionSSL,+    baselineContextSSL,+    modifyContextSSL,+    establishConnection,++    -- * Testing support+    makeConnection,+    Headers,+    getHeaders,+    getHeadersFull,+    packBoundary,++    -- * Deprecated+    concatHandler,+    concatHandler',+    getRequestHeaders+) where++import Network.Http.Types++import Network.Http.Connection+import Network.Http.Inconvenience
+ lib/Network/Http/Connection.hs view
@@ -0,0 +1,679 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2021 Athae Eredh Siniath and Others+--+-- The code in this file, and the program it is a part of, is+-- made available to you by its authors as open source software:+-- you can redistribute it and/or modify it under the terms of+-- the BSD licence.+--+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DoAndIfThenElse #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK hide, not-home #-}++module Network.Http.Connection (+    Connection (..),+    -- constructors only for testing+    makeConnection,+    withConnection,+    openConnection,+    openConnectionSSL,+    openConnectionUnix,+    closeConnection,+    getHostname,+    getRequestHeaders,+    getHeadersFull,+    sendRequest,+    receiveResponse,+    receiveResponseRaw,+    unsafeReceiveResponse,+    UnexpectedCompression,+    emptyBody,+    simpleBody,+    fileBody,+    inputStreamBody,+    debugHandler,+    simpleHandler,+    concatHandler,+) where++import Blaze.ByteString.Builder (Builder)+import qualified Blaze.ByteString.Builder as Builder (+    flush,+    fromByteString,+    toByteString,+ )+import qualified Blaze.ByteString.Builder.HTTP as Builder (chunkedTransferEncoding, chunkedTransferTerminator)+import Control.Exception (bracket)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as S+import Network.Socket+import OpenSSL (withOpenSSL)+import OpenSSL.Session (SSL, SSLContext)+import qualified OpenSSL.Session as SSL+import System.IO.Streams (InputStream, OutputStream, stdout)+import qualified System.IO.Streams as Streams+import qualified System.IO.Streams.SSL as Streams hiding (connect)++#if !MIN_VERSION_base(4,8,0)+import Data.Monoid (mappend, mempty)+#endif++import Network.Http.Internal+import Network.Http.ResponseParser++{- |+A connection to a web server.+-}+data Connection = Connection+    { -- | will be used as the Host: header in the HTTP request.+      cHost :: ByteString+    , -- | called when the connection should be closed.+      cClose :: IO ()+    , cOut :: OutputStream Builder+    , cIn :: InputStream ByteString+    }++instance Show Connection where+    show c =+        {-# SCC "Connection.show" #-}+        concat+            [ "Host: "+            , S.unpack $ cHost c+            , "\n"+            ]++{- |+Create a raw Connection object from the given parts. This is+primarily of use when teseting, for example:++> fakeConnection :: IO Connection+> fakeConnection = do+>     o  <- Streams.nullOutput+>     i  <- Streams.nullInput+>     c  <- makeConnection "www.example.com" (return()) o i+>     return c++is an idiom we use frequently in testing and benchmarking, usually+replacing the InputStream with something like:++>     x' <- S.readFile "properly-formatted-response.txt"+>     i  <- Streams.fromByteString x'++If you're going to do that, keep in mind that you /must/ have CR-LF+pairs after each header line and between the header and body to+be compliant with the HTTP protocol; otherwise, parsers will+reject your message.+-}+makeConnection ::+    -- | will be used as the @Host:@ header in the HTTP request.+    ByteString ->+    -- | an action to be called when the connection is terminated.+    IO () ->+    -- | write end of the HTTP client-server connection.+    OutputStream ByteString ->+    -- | read end of the HTTP client-server connection.+    InputStream ByteString ->+    IO Connection+makeConnection h c o1 i = do+    o2 <- Streams.builderStream o1+    return $! Connection h c o2 i++{- |+Given an @IO@ action producing a 'Connection', and a computation+that needs one, runs the computation, cleaning up the+@Connection@ afterwards.++>     x <- withConnection (openConnection "s3.example.com" 80) $ (\c -> do+>         let q = buildRequest1 $ do+>             http GET "/bucket42/object/149"+>         sendRequest c q emptyBody+>         ...+>         return "blah")++which can make the code making an HTTP request a lot more+straight-forward.++Wraps @Control.Exception@'s 'Control.Exception.bracket'.+-}+withConnection :: IO Connection -> (Connection -> IO γ) -> IO γ+withConnection mkC =+    bracket mkC closeConnection++{- |+In order to make a request you first establish the TCP+connection to the server over which to send it.++Ordinarily you would supply the host part of the URL here and it will+be used as the value of the HTTP 1.1 @Host:@ field. However, you can+specify any server name or IP addresss and set the @Host:@ value+later with 'Network.Http.Client.setHostname' when building the+request.++Usage is as follows:++>     c <- openConnection "localhost" 80+>     ...+>     closeConnection c++More likely, you'll use 'withConnection' to wrap the call in order+to ensure finalization.++HTTP pipelining is supported; you can reuse the connection to a+web server, but it's up to you to ensure you match the number of+requests sent to the number of responses read, and to process those+responses in order. This is all assuming that the /server/ supports+pipelining; be warned that not all do. Web browsers go to+extraordinary lengths to probe this; you probably only want to do+pipelining under controlled conditions. Otherwise just open a new+connection for subsequent requests.+-}+openConnection :: Hostname -> Port -> IO Connection+openConnection h1' p = do+    is <- getAddrInfo (Just hints) (Just h1) (Just $ show p)+    let addr = head is+    let a = addrAddress addr+    s <- socket (addrFamily addr) Stream defaultProtocol++    connect s a+    (i, o1) <- Streams.socketToStreams s++    o2 <- Streams.builderStream o1++    return+        Connection+            { cHost = h2'+            , cClose = close s+            , cOut = o2+            , cIn = i+            }+  where+    hints =+        defaultHints+            { addrFlags = [AI_NUMERICSERV]+            , addrSocketType = Stream+            }+    h2' =+        if p == 80+            then h1'+            else S.concat [h1', ":", S.pack $ show p]+    h1 = S.unpack h1'++{- |+Open a secure connection to a web server.++> import OpenSSL (withOpenSSL)+>+> main :: IO ()+> main = do+>     ctx <- baselineContextSSL+>     c <- openConnectionSSL ctx "api.github.com" 443+>     ...+>     closeConnection c++If you want to tune the parameters used in making SSL connections,+manually specify certificates, etc, then setup your own context:++> import OpenSSL.Session (SSLContext)+> import qualified OpenSSL.Session as SSL+>+>     ...+>     ctx <- SSL.context+>     ...++See "OpenSSL.Session".++Crypto is as provided by the system @openssl@ library, as wrapped+by the @HsOpenSSL@ package and @openssl-streams@.++/There is no longer a need to call @withOpenSSL@ explicitly; the+initialization is invoked once per process for you/+-}+openConnectionSSL :: SSLContext -> Hostname -> Port -> IO Connection+openConnectionSSL ctx h1' p = withOpenSSL $ do+    is <- getAddrInfo Nothing (Just h1) (Just $ show p)++    let a = addrAddress $ head is+        f = addrFamily $ head is+    s <- socket f Stream defaultProtocol++    connect s a++    ssl <- SSL.connection ctx s+    SSL.setTlsextHostName ssl h1+    SSL.connect ssl++    (i, o1) <- Streams.sslToStreams ssl++    o2 <- Streams.builderStream o1++    return+        Connection+            { cHost = h2'+            , cClose = closeSSL s ssl+            , cOut = o2+            , cIn = i+            }+  where+    h2' :: ByteString+    h2' =+        if p == 443+            then h1'+            else S.concat [h1', ":", S.pack $ show p]+    h1 = S.unpack h1'++closeSSL :: Socket -> SSL -> IO ()+closeSSL s ssl = do+    SSL.shutdown ssl SSL.Unidirectional+    close s++{- |+Open a connection to a Unix domain socket.++> main :: IO ()+> main = do+>     c <- openConnectionUnix "/var/run/docker.sock"+>     ...+>     closeConnection c+-}+openConnectionUnix :: FilePath -> IO Connection+openConnectionUnix path = do+    let a = SockAddrUnix path+    s <- socket AF_UNIX Stream defaultProtocol++    connect s a+    (i, o1) <- Streams.socketToStreams s++    o2 <- Streams.builderStream o1++    return+        Connection+            { cHost = path'+            , cClose = close s+            , cOut = o2+            , cIn = i+            }+  where+    path' = S.pack path++{- |+Having composed a 'Request' object with the headers and metadata for+this connection, you can now send the request to the server, along+with the entity body, if there is one. For the rather common case of+HTTP requests like 'GET' that don't send data, use 'emptyBody' as the+output stream:++>     sendRequest c q emptyBody++For 'PUT' and 'POST' requests, you can use 'fileBody' or+'inputStreamBody' to send content to the server, or you can work with+the @io-streams@ API directly:++>     sendRequest c q (\o ->+>         Streams.write (Just (Builder.fromString "Hello World\n")) o)+-}++{-+    I would like to enforce the constraints on the Empty and Static+    cases shown here, but those functions take OutputStream ByteString,+    and we are of course working in OutputStream Builder by that point.+-}+sendRequest :: Connection -> Request -> (OutputStream Builder -> IO α) -> IO α+sendRequest c q handler = do+    -- write the headers++    Streams.write (Just msg) o2++    -- deal with the expect-continue mess++    e2 <- case t of+        Normal -> do+            return e+        Continue -> do+            Streams.write (Just Builder.flush) o2++            p <- readResponseHeader i++            case getStatusCode p of+                100 -> do+                    -- ok to send+                    return e+                _ -> do+                    -- put the response back+                    Streams.unRead (rsp p) i+                    return Empty++    -- write the body, if there is one++    x <- case e2 of+        Empty -> do+            o3 <- Streams.nullOutput+            y <- handler o3+            return y+        Chunking -> do+            o3 <- Streams.contramap Builder.chunkedTransferEncoding o2+            y <- handler o3+            Streams.write (Just Builder.chunkedTransferTerminator) o2+            return y+        (Static _) -> do+            -- o3 <- Streams.giveBytes (fromIntegral n :: Int64) o2+            y <- handler o2+            return y++    -- push the stream out by flushing the output buffers++    Streams.write (Just Builder.flush) o2++    return x+  where+    o2 = cOut c+    e = qBody q+    t = qExpect q+    msg = composeRequestBytes q h'+    h' = cHost c+    i = cIn c+    rsp p = Builder.toByteString $ composeResponseBytes p++{- |+Get the virtual hostname that will be used as the @Host:@ header in+the HTTP 1.1 request. Per RFC 2616 § 14.23, this will be of the form+@hostname:port@ if the port number is other than the default, ie 80+for HTTP.+-}+getHostname :: Connection -> Request -> ByteString+getHostname c q =+    case qHost q of+        Just h' -> h'+        Nothing -> cHost c++{-# DEPRECATED getRequestHeaders "use retrieveHeaders . getHeadersFull instead" #-}+getRequestHeaders :: Connection -> Request -> [(ByteString, ByteString)]+getRequestHeaders c q =+    ("Host", getHostname c q) : kvs+  where+    h = qHeaders q+    kvs = retrieveHeaders h++{- |+Get the headers that will be sent with this request. You likely won't+need this but there are some corner cases where people need to make+calculations based on all the headers before they go out over the wire.++If you'd like the request headers as an association list, import the header+functions:++> import Network.Http.Types++then use 'Network.Http.Types.retreiveHeaders' as follows:++>>> let kvs = retreiveHeaders $ getHeadersFull c q+>>> :t kvs+:: [(ByteString, ByteString)]+-}+getHeadersFull :: Connection -> Request -> Headers+getHeadersFull c q =+    h'+  where+    h = qHeaders q+    h' = updateHeader h "Host" (getHostname c q)++{- |+Handle the response coming back from the server. This function+hands control to a handler function you supply, passing you the+'Response' object with the response headers and an 'InputStream'+containing the entity body.++For example, if you just wanted to print the first chunk of the+content from the server:++>     receiveResponse c (\p i -> do+>         m <- Streams.read i+>         case m of+>             Just bytes -> putStr bytes+>             Nothing    -> return ())++Obviously, you can do more sophisticated things with the+'InputStream', which is the whole point of having an @io-streams@+based HTTP client library.++The final value from the handler function is the return value of+@receiveResponse@, if you need it.++Throws 'UnexpectedCompression' if it doesn't know how to handle the+compression format used in the response.+-}++{-+    The reponse body coming from the server MUST be fully read, even+    if (especially if) the users's handler doesn't consume it all.+    This is necessary to maintain the HTTP protocol invariants;+    otherwise pipelining would not work. It's not entirely clear+    *which* InputStream is being drained here; the underlying+    InputStream ByteString in Connection remains unconsumed beyond the+    threshold of the current response, which is exactly what we need.+-}+receiveResponse :: Connection -> (Response -> InputStream ByteString -> IO β) -> IO β+receiveResponse c handler = do+    p <- readResponseHeader i+    i' <- readResponseBody p i++    x <- handler p i'++    Streams.skipToEof i'++    return x+  where+    i = cIn c++{- |+This is a specialized variant of 'receiveResponse' that /explicitly/ does+not handle the content encoding of the response body stream (it will not+decompress anything). Unless you really want the raw gzipped content coming+down from the server, use @receiveResponse@.+-}++{-+    See notes at receiveResponse.+-}+receiveResponseRaw :: Connection -> (Response -> InputStream ByteString -> IO β) -> IO β+receiveResponseRaw c handler = do+    p <- readResponseHeader i+    let p' =+            p+                { pContentEncoding = Identity+                }++    i' <- readResponseBody p' i++    x <- handler p i'++    Streams.skipToEof i'++    return x+  where+    i = cIn c++{- |+Handle the response coming back from the server. This function+is the same as receiveResponse, but it does not consume the body for+you after the handler is done.  This means that it can only be safely used+if the handler will fully consume the body, there is no body, or when+the connection is not being reused (no pipelining).+-}+unsafeReceiveResponse :: Connection -> (Response -> InputStream ByteString -> IO β) -> IO β+unsafeReceiveResponse c handler = do+    p <- readResponseHeader i+    i' <- readResponseBody p i++    handler p i'+  where+    i = cIn c++{- |+Use this for the common case of the HTTP methods that only send+headers and which have no entity body, i.e. 'GET' requests.+-}+emptyBody :: OutputStream Builder -> IO ()+emptyBody _ = return ()++{- |+Sometimes you just want to send some bytes to the server as a the body of your+request. This is easy to use, but if you're doing anything massive use+'inputStreamBody'; if you're sending a file use 'fileBody'; if you have an+object that needs to be sent as JSON use 'jsonBody'+-}+simpleBody :: ByteString -> OutputStream Builder -> IO ()+simpleBody x' o = do+    let b = Builder.fromByteString x'+    Streams.write (Just b) o++{- |+Specify a local file to be sent to the server as the body of the+request.++You use this partially applied:++>     sendRequest c q (fileBody "/etc/passwd")++Note that the type of @(fileBody \"\/path\/to\/file\")@ is just what+you need for the third argument to 'sendRequest', namely++>>> :t filePath "hello.txt"+:: OutputStream Builder -> IO ()+-}++{-+    Relies on Streams.withFileAsInput generating (very) large chunks [which it+    does]. A more efficient way to do this would be interesting.+-}+fileBody :: FilePath -> OutputStream Builder -> IO ()+fileBody p o = do+    Streams.withFileAsInput p (\i -> inputStreamBody i o)++{- |+Read from a pre-existing 'InputStream' and pipe that through to the+connection to the server. This is useful in the general case where+something else has handed you stream to read from and you want to use+it as the entity body for the request.++You use this partially applied:++>     i <- getStreamFromVault                    -- magic, clearly+>     sendRequest c q (inputStreamBody i)++This function maps "Builder.fromByteString" over the input, which will+be efficient if the ByteString chunks are large.+-}++{-+    Note that this has to be 'supply' and not 'connect' as we do not+    want the end of stream to prematurely terminate the chunked encoding+    pipeline!+-}+inputStreamBody :: InputStream ByteString -> OutputStream Builder -> IO ()+inputStreamBody i1 o = do+    i2 <- Streams.map Builder.fromByteString i1+    Streams.supply i2 o++{- |+Print the response headers and response body to @stdout@. You can+use this with 'receiveResponse' or one of the convenience functions+when testing. For example, doing:++>     c <- openConnection "kernel.operationaldynamics.com" 58080+>+>     let q = buildRequest1 $ do+>                 http GET "/time"+>+>     sendRequest c q emptyBody+>+>     receiveResponse c debugHandler++would print out:++> HTTP/1.1 200 OK+> Transfer-Encoding: chunked+> Content-Type: text/plain+> Vary: Accept-Encoding+> Server: Snap/0.9.2.4+> Content-Encoding: gzip+> Date: Mon, 21 Jan 2013 06:13:37 GMT+>+> Mon 21 Jan 13, 06:13:37.303Z++or thereabouts.+-}+debugHandler :: Response -> InputStream ByteString -> IO ()+debugHandler p i = do+    S.putStr $ S.filter (/= '\r') $ Builder.toByteString $ composeResponseBytes p+    Streams.connect i stdout++{- |+Sometimes you just want the entire response body as a single blob.+This function concatonates all the bytes from the response into a+ByteString. If using the main @http-streams@ API, you would use it+as follows:++>    ...+>    x' <- receiveResponse c simpleHandler+>    ...++The methods in the convenience API all take a function to handle the+response; this function is passed directly to the 'receiveResponse'+call underlying the request. Thus this utility function can be used+for 'get' as well:++>    x' <- get "http://www.example.com/document.txt" simpleHandler++Either way, the usual caveats about allocating a+single object from streaming I/O apply: do not use this if you are+not absolutely certain that the response body will fit in a+reasonable amount of memory.++Note that this function makes no discrimination based on the+response's HTTP status code. You're almost certainly better off+writing your own handler function.+-}+simpleHandler :: Response -> InputStream ByteString -> IO ByteString+simpleHandler _ i1 = do+    i2 <- Streams.map Builder.fromByteString i1+    x <- Streams.fold mappend mempty i2+    return $ Builder.toByteString x++concatHandler :: Response -> InputStream ByteString -> IO ByteString+concatHandler = simpleHandler+{-# DEPRECATED concatHandler "Use simpleHandler instead" #-}++{- |+Shutdown the connection. You need to call this release the+underlying socket file descriptor and related network resources. To+do so reliably, use this in conjunction with 'openConnection' in a+call to 'Control.Exception.bracket':++> --+> -- Make connection, cleaning up afterward+> --+>+> foo :: IO ByteString+> foo = bracket+>    (openConnection "localhost" 80)+>    (closeConnection)+>    (doStuff)+>+> --+> -- Actually use Connection to send Request and receive Response+> --+>+> doStuff :: Connection -> IO ByteString++or, just use 'withConnection'.++While returning a ByteString is probably the most common use case,+you could conceivably do more processing of the response in 'doStuff'+and have it and 'foo' return a different type.+-}+closeConnection :: Connection -> IO ()+closeConnection c = cClose c
+ lib/Network/Http/Inconvenience.hs view
@@ -0,0 +1,753 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2021 Athae Eredh Siniath and Others+--+-- The code in this file, and the program it is a part of, is+-- made available to you by its authors as open source software:+-- you can redistribute it and/or modify it under the terms of+-- the BSD licence.+--+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_HADDOCK hide, not-home #-}++module Network.Http.Inconvenience (+    URL,+    modifyContextSSL,+    establishConnection,+    get,+    post,+    postForm,+    encodedFormBody,+    multipartFormBody,+    Part,+    simplePart,+    filePart,+    inputStreamPart,+    put,+    baselineContextSSL,+    simpleHandler',+    concatHandler',+    jsonBody,+    jsonHandler,+    TooManyRedirects (..),+    HttpClientError (..),+    -- for testing+    splitURI,+    parseURL,+) where++import Blaze.ByteString.Builder (Builder)+import qualified Blaze.ByteString.Builder as Builder (+    fromByteString,+    fromLazyByteString,+    fromWord8,+    toByteString,+ )+import qualified Blaze.ByteString.Builder.Char8 as Builder (fromString)+import Control.Exception (Exception, bracket, throw)+import Data.Aeson (FromJSON, Result (..), ToJSON, encode, fromJSON)+import Data.Aeson.Parser (json')+import Data.Bits (Bits (..))+import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as S+import Data.ByteString.Internal (c2w, w2c)+import Data.Char (intToDigit)+import Data.HashSet (HashSet)+import qualified Data.HashSet as HashSet+import Data.IORef (IORef, newIORef, readIORef, writeIORef)+import Data.List (intersperse)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Typeable (Typeable)+import Data.Word (Word16)+import GHC.Exts+import GHC.Word (Word8 (..))+import Network.URI (+    URI (..),+    URIAuth (..),+    escapeURIString,+    isAbsoluteURI,+    isAllowedInURI,+    parseRelativeReference,+    parseURI,+    uriToString,+ )+import OpenSSL (withOpenSSL)+import OpenSSL.Session (SSLContext)+import qualified OpenSSL.Session as SSL+import System.FilePath.Posix (takeFileName)+import System.IO.Streams (InputStream, OutputStream)+import qualified System.IO.Streams as Streams+import qualified System.IO.Streams.Attoparsec as Streams+import System.IO.Unsafe (unsafePerformIO)++#if !MIN_VERSION_base(4,8,0)+import Data.Monoid (Monoid (..), mappend)+#endif++import Network.Http.Connection+import Network.Http.Internal+import Network.Http.RequestBuilder++-- (see also http://downloads.haskell.org/~ghc/8.4.2/docs/html/users_guide/phases.html#standard-cpp-macros+-- for a list of predefined CPP macros provided by GHC and/or Cabal; see also the cabal user's guide)+#if defined(linux_HOST_OS) || defined(freebsd_HOST_OS)+import System.Directory (doesDirectoryExist)+#endif++type URL = ByteString++------------------------------------------------------------------------------++{- |+URL-escapes a string (see+<http://tools.ietf.org/html/rfc2396.html#section-2.4>)+-}+urlEncode :: ByteString -> URL+urlEncode = Builder.toByteString . urlEncodeBuilder+{-# INLINE urlEncode #-}++{- |+URL-escapes a string (see+<http://tools.ietf.org/html/rfc2396.html#section-2.4>) into a 'Builder'.+-}+urlEncodeBuilder :: ByteString -> Builder+urlEncodeBuilder = go mempty+  where+    go !b !s = maybe b' esc (S.uncons y)+      where+        (x, y) = S.span (flip HashSet.member urlEncodeTable) s+        b' = b `mappend` Builder.fromByteString x+        esc (c, r) =+            let b'' =+                    if c == ' '+                        then b' `mappend` Builder.fromWord8 (c2w '+')+                        else b' `mappend` hexd c+             in go b'' r++hexd :: Char -> Builder+hexd c0 =+    Builder.fromWord8 (c2w '%') `mappend` Builder.fromWord8 hi+        `mappend` Builder.fromWord8 low+  where+    !c = c2w c0+    toDigit = c2w . intToDigit+    !low = toDigit $ fromEnum $ c .&. 0xf+    !hi = toDigit $ (c .&. 0xf0) `shiftr` 4++    shiftr (W8# a#) (I# b#) = I# (word2Int# (uncheckedShiftRL'# a# b#))+#if MIN_VERSION_base(4,16,0)+    uncheckedShiftRL'# a# b# = word8ToWord# (uncheckedShiftRLWord8# a# b#)+#else+    uncheckedShiftRL'# = uncheckedShiftRL#+#endif++urlEncodeTable :: HashSet Char+urlEncodeTable = HashSet.fromList $! filter f $! map w2c [0 .. 255]+  where+    f c+        | c >= 'A' && c <= 'Z' = True+        | c >= 'a' && c <= 'z' = True+        | c >= '0' && c <= '9' = True+    f c = c `elem` ("$-_.!~*'()," :: String)++------------------------------------------------------------------------------++{-+    The default SSLContext used by the convenience APIs in the http-streams+    library. This is a kludge, unsafe bad yada yada. The technique, however,+    was described on a Haskell Wiki page, so that makes it an officially+    supported kludge. The justification for doing this is a) the functions+    accessing this IORef are themselves all in the IO monad, and b) these+    contortions are necessary to allow the library to be used for https:// URLs+    *without* requiring the developer to do 'withOpenSSL'.+-}+global :: IORef SSLContext+global = unsafePerformIO $ do+    ctx <- baselineContextSSL+    newIORef ctx+{-# NOINLINE global #-}++{- |+Modify the context being used to configure the SSL tunnel used by+the convenience API functions to make @https://@ connections. The+default is that setup by 'baselineContextSSL'.+-}+modifyContextSSL :: (SSLContext -> IO SSLContext) -> IO ()+modifyContextSSL f = do+    ctx <- readIORef global+    ctx' <- f ctx+    writeIORef global ctx'++{- |+Given a URL, work out whether it is normal, secure, or unix domain,+and then open the connection to the webserver including setting the+appropriate default port if one was not specified in the URL. This+is what powers the convenience API, but you may find it useful in+composing your own similar functions.++For example (on the assumption that your server behaves when given+an absolute URI as the request path), this will open a connection+to server @www.example.com@ port @443@ and request @/photo.jpg@:++>     let url = "https://www.example.com/photo.jpg"+>+>     c <- establishConnection url+>     let q = buildRequest1 $ do+>                 http GET url+>     ...+-}+establishConnection :: URL -> IO (Connection)+establishConnection r' = do+    establish u+  where+    u = parseURL r'+{-# INLINE establishConnection #-}++establish :: URI -> IO (Connection)+establish u =+    case scheme of+        "http:" -> do+            openConnection host port+        "https:" -> do+            ctx <- readIORef global+            openConnectionSSL ctx host ports+        "unix:" -> do+            openConnectionUnix $ uriPath u+        _ -> error ("Unknown URI scheme " ++ scheme)+  where+    scheme = uriScheme u++    auth = case uriAuthority u of+        Just x -> x+        Nothing -> URIAuth "" "localhost" ""++    host = S.pack (uriRegName auth)+    port = case uriPort auth of+        "" -> 80+        _ -> read $ tail $ uriPort auth :: Word16+    ports = case uriPort auth of+        "" -> 443+        _ -> read $ tail $ uriPort auth :: Word16++{- |+Creates a basic SSL context. This is the SSL context used if you make an+@\"https:\/\/\"@ request using one of the convenience functions. It+configures OpenSSL to use the default set of ciphers.++On Linux, OpenBSD and FreeBSD systems, this function also configures+OpenSSL to verify certificates using the system/distribution supplied+certificate authorities' certificates++On other systems, /no certificate validation is performed/ by the+generated 'SSLContext' because there is no canonical place to find+the set of system certificates. When using this library on such system,+you are encouraged to install the system+certificates somewhere and create your own 'SSLContext'.+-}++{-+    We would like to turn certificate verification on for everyone, but+    this has proved contingent on leveraging platform specific mechanisms+    to reach the certificate store. That logic should probably be in+    hsopenssl, but feel free to change this as appropriate for your OS.+-}+baselineContextSSL :: IO SSLContext+baselineContextSSL = withOpenSSL $ do+    ctx <- SSL.context+    SSL.contextSetDefaultCiphers ctx+#if defined(darwin_HOST_OS)+    SSL.contextSetVerificationMode ctx SSL.VerifyNone+#elif defined(mingw32_HOST_OS)+    SSL.contextSetVerificationMode ctx SSL.VerifyNone+#elif defined(freebsd_HOST_OS)+    SSL.contextSetCAFile ctx "/usr/local/etc/ssl/cert.pem"+    SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer True True Nothing+#elif defined(openbsd_HOST_OS)+    SSL.contextSetCAFile ctx "/etc/ssl/cert.pem"+    SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer True True Nothing+#else+    fedora <- doesDirectoryExist "/etc/pki/tls"+    if fedora+        then do+            SSL.contextSetCAFile ctx "/etc/pki/tls/certs/ca-bundle.crt"+        else do+            SSL.contextSetCADirectory ctx "/etc/ssl/certs"+    SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer True True Nothing+#endif+    return ctx++parseURL :: URL -> URI+parseURL r' =+    case parseURI r of+        Just u -> u+        Nothing -> error ("Can't parse URI " ++ r)+  where+    r = escapeURIString isAllowedInURI $ T.unpack $ T.decodeUtf8 r'++------------------------------------------------------------------------------++{-+    Account for bug where "http://www.example.com" is parsed with no+    path element, resulting in an illegal HTTP request line.+-}++pathFrom :: URI -> ByteString+pathFrom u = case url of+    "" -> "/"+    _ -> url+  where+    url =+        T.encodeUtf8 $! T.pack+            $! concat [uriPath u, uriQuery u, uriFragment u]++------------------------------------------------------------------------------++{- |+Issue an HTTP GET request and pass the resultant response to the+supplied handler function. This code will silently follow redirects,+to a maximum depth of 5 hops.++The handler function is as for 'receiveResponse', so you can use one+of the supplied convenience handlers if you're in a hurry:++>     x' <- get "http://www.bbc.co.uk/news/" concatHandler++But as ever the disadvantage of doing this is that you're not doing+anything intelligent with the HTTP response status code. If you want+an exception raised in the event of a non @2xx@ response, you can use:++>     x' <- get "http://www.bbc.co.uk/news/" concatHandler'++but for anything more refined you'll find it easy to simply write+your own handler function.++Throws 'TooManyRedirects' if more than 5 redirects are thrown.+-}+get ::+    -- | Resource to GET from.+    URL ->+    -- | Handler function to receive the response from the server.+    (Response -> InputStream ByteString -> IO β) ->+    IO β+get r' handler = getN 0 r' handler++getN n r' handler = do+    bracket+        (establish u)+        (teardown)+        (process)+  where+    teardown = closeConnection++    u = parseURL r'++    q = buildRequest1 $ do+        http GET (pathFrom u)+        setAccept "*/*"++    process c = do+        sendRequest c q emptyBody++        receiveResponse c (wrapRedirect u n handler)++{-+    This is fairly simple-minded. Improvements could include reusing+    the Connection if the redirect is to the same host, and closing+    the original Connection if it is not. These are both things that+    can be done manually if using the full API, so not worried about+    it for now.+-}++wrapRedirect ::+    URI ->+    Int ->+    (Response -> InputStream ByteString -> IO β) ->+    Response ->+    InputStream ByteString ->+    IO β+wrapRedirect u n handler p i = do+    if (s == 301 || s == 302 || s == 303 || s == 307)+        then case lm of+            Just l -> getN n' (splitURI u l) handler+            Nothing -> handler p i+        else handler p i+  where+    s = getStatusCode p+    lm = getHeader p "Location"+    !n' =+        if n < 5+            then n + 1+            else throw $! TooManyRedirects n++splitURI :: URI -> URL -> URL+splitURI old new' =+    let new = S.unpack new'+     in if isAbsoluteURI new+            then new'+            else+                let rel = parseRelativeReference new+                 in case rel of+                        Nothing -> new'+                        Just x ->+                            S.pack $+                                uriToString+                                    id+                                    old+                                        { uriPath = uriPath x+                                        , uriQuery = uriQuery x+                                        , uriFragment = uriFragment x+                                        }+                                    ""++data TooManyRedirects = TooManyRedirects Int+    deriving (Typeable, Show, Eq)++instance Exception TooManyRedirects++{- |+Send content to a server via an HTTP POST request. Use this+function if you have an 'OutputStream' with the body content.+-}+post ::+    -- | Resource to POST to.+    URL ->+    -- | MIME type of the request body being sent.+    ContentType ->+    -- | Handler function to write content to server.+    (OutputStream Builder -> IO α) ->+    -- | Handler function to receive the response from the server.+    (Response -> InputStream ByteString -> IO β) ->+    IO β+post r' t body handler = do+    bracket+        (establish u)+        (teardown)+        (process)+  where+    teardown = closeConnection++    u = parseURL r'++    q = buildRequest1 $ do+        http POST (pathFrom u)+        setAccept "*/*"+        setContentType t++    process c = do+        _ <- sendRequest c q body++        x <- receiveResponse c handler+        return x++{- |+Send form data to a server via an HTTP POST request. This is the+usual use case; most services expect the body to be MIME type+@application/x-www-form-urlencoded@ as this is what conventional+web browsers send on form submission. If you want to POST to a URL+with an arbitrary Content-Type, use 'post'.+-}+postForm ::+    -- | Resource to POST to.+    URL ->+    -- | List of name=value pairs. Will be sent URL-encoded.+    [(ByteString, ByteString)] ->+    -- | Handler function to receive the response from the server.+    (Response -> InputStream ByteString -> IO β) ->+    IO β+postForm r' nvs handler = do+    bracket+        (establish u)+        (teardown)+        (process)+  where+    teardown = closeConnection++    u = parseURL r'++    q = buildRequest1 $ do+        http POST (pathFrom u)+        setAccept "*/*"+        setContentType "application/x-www-form-urlencoded"++    process c = do+        _ <- sendRequest c q (encodedFormBody nvs)++        x <- receiveResponse c handler+        return x++{- |+Specify name/value pairs to be sent to the server in the manner+used by web browsers when submitting a form via a POST request.+Parameters will be URL encoded per RFC 2396 and combined into a+single string which will be sent as the body of your request.++You use this partially applied:++>     let nvs = [("name","Kermit"),+>                ("type","frog")]+>                ("role","stagehand")]+>+>     sendRequest c q (encodedFormBody nvs)++Note that it's going to be up to you to call 'setContentType' with+a value of @\"application/x-www-form-urlencoded\"@ when building the+Request object; the 'postForm' convenience (which uses this+@encodedFormBody@ function) takes care of this for you, obviously.+-}+encodedFormBody :: [(ByteString, ByteString)] -> OutputStream Builder -> IO ()+encodedFormBody nvs o = do+    Streams.write (Just b) o+  where+    b = mconcat $ intersperse (Builder.fromString "&") $ map combine nvs++    combine :: (ByteString, ByteString) -> Builder+    combine (n', v') = mconcat [urlEncodeBuilder n', Builder.fromString "=", urlEncodeBuilder v']++{- |+Build a list of parts into an upload body.++You use this partially applied:++>     boundary <- randomBoundary+>+>     let q = buildRequest1 $ do+>           http POST "/api/v1/upload"+>           setContentMultipart boundary+>+>     let parts =+>             [ simplePart "metadata" Nothing metadata+>             , filePart "submission" (Just "audio/wav") filepath+>             ]+>+>     sendRequest c q (multipartFormBody boundary parts)++You /must/ have called 'setContentMultipart' when forming the request or the+request body you are sending will be invalid and (obviously) you must pass in+that same 'Boundary' value when calling this function.+-}+multipartFormBody :: Boundary -> [Part] -> OutputStream Builder -> IO ()+multipartFormBody boundary parts o = do+    mapM_ handlePart parts+    handleEnding+  where+    handlePart :: Part -> IO ()+    handlePart (Part field possibleContentType possibleFilename action) = do+        let h' = composeMultipartBytes boundary field possibleFilename possibleContentType+        Streams.write (Just h') o+        action o++    handleEnding :: IO ()+    handleEnding = do+        Streams.write (Just (composeMultipartEnding boundary)) o++{- |+Information about each of the parts of a @multipart/form-data@ form upload.+Build these with 'simplePart', 'filePart', or 'inputStreamPart'.+-}+data Part = Part+    { partFieldName :: FieldName+    , partContentType :: Maybe ContentType+    , partFilename :: Maybe FilePath+    , partDataHandler :: OutputStream Builder -> IO ()+    }++{- |+Given a simple static set of bytes, send them as a part in a multipart form+upload. You need to specify the name of the field for the form, and optionally+can supply a MIME content-type.+-}+simplePart :: FieldName -> Maybe ContentType -> ByteString -> Part+simplePart name possibleContentType x' =+    let action o = do+            i1 <- Streams.fromByteString x'+            i2 <- Streams.map Builder.fromByteString i1+            Streams.supply i2 o+     in Part name possibleContentType Nothing action++{- |+The most common case in using multipart form data is to upload a file. Specify+the name of the field, optionally a MIME content-type, and then the path to+the file to be transmitted. The filename (without directory) will be used to+name the file to the server.+-}+filePart :: FieldName -> Maybe ContentType -> FilePath -> Part+filePart name possibleContentType path =+    let action o = do+            Streams.withFileAsInput+                path+                ( \i1 -> do+                    i2 <- Streams.map Builder.fromByteString i1+                    Streams.supply i2 o+                )++        filename = takeFileName path+     in Part name possibleContentType (Just filename) action++{- |+Build a piece of a multipart submission from an 'InputStream'. You need to+specify a field name for this piece of the submission, and can optionally+indicate the MIME type and a filename (if what you are sending is going to be+interpreted as a file).+-}+inputStreamPart :: FieldName -> Maybe ContentType -> Maybe FilePath -> InputStream ByteString -> Part+inputStreamPart name possibleContentType possilbeFilename i1 =+    let action o = do+            i2 <- Streams.map Builder.fromByteString i1+            Streams.supply i2 o+     in Part name possibleContentType possilbeFilename action++{- |+Place content on the server at the given URL via an HTTP PUT+request, specifying the content type and a function to write the+content to the supplied 'OutputStream'. You might see:++>     put "http://s3.example.com/bucket42/object149" "text/plain"+>         (fileBody "hello.txt") (\p i -> do+>             putStr $ show p+>             Streams.connect i stdout)+-}+put ::+    -- | Resource to PUT to.+    URL ->+    -- | MIME type of the request body being sent.+    ContentType ->+    -- | Handler function to write content to server.+    (OutputStream Builder -> IO α) ->+    -- | Handler function to receive the response from the server.+    (Response -> InputStream ByteString -> IO β) ->+    IO β+put r' t body handler = do+    bracket+        (establish u)+        (teardown)+        (process)+  where+    teardown = closeConnection++    u = parseURL r'++    q = buildRequest1 $ do+        http PUT (pathFrom u)+        setAccept "*/*"+        setHeader "Content-Type" t++    process c = do+        _ <- sendRequest c q body++        x <- receiveResponse c handler+        return x++{- |+A special case of 'concatHandler', this function will return the+entire response body as a single ByteString, but will throw+'HttpClientError' if the response status code was other than @2xx@.+-}+simpleHandler' :: Response -> InputStream ByteString -> IO ByteString+simpleHandler' p i =+    if s >= 300+        then throw (HttpClientError s m)+        else simpleHandler p i+  where+    s = getStatusCode p+    m = getStatusMessage p++concatHandler' :: Response -> InputStream ByteString -> IO ByteString+concatHandler' = simpleHandler'++data HttpClientError = HttpClientError Int ByteString+    deriving (Typeable)++instance Exception HttpClientError++instance Show HttpClientError where+    show (HttpClientError s msg) = Prelude.show s ++ " " ++ S.unpack msg++{-+    There should probably also be HttpServerError and maybe even+    HttpRedirectError, but as these names don't seem to show up+    in the runtime when raised, not sure it's worth the bother. It's+    not like we'd want anything different in their Show instances.+-}++{- |+If you've got an object of a type with a 'ToJSON' instance and you need to+send that object as JSON up to a web service API, this can help.++You use this partially applied:++>    sendRequest c q (jsonBody thing)+-}+jsonBody :: ToJSON a => a -> OutputStream Builder -> IO ()+jsonBody thing o = do+    let b = Builder.fromLazyByteString (encode thing)+    Streams.write (Just b) o++{- |+If you're working with a data stream that is in @application/json@,+then chances are you're using @aeson@ to handle the JSON to Haskell+decoding. If so, then this helper function might be of use.++>     v <- get "http://api.example.com/v1/" jsonHandler++This function feeds the input body to the 'Data.Aeson.Parser.json''+@attoparsec@ Parser in order to get the aeson Value type. This is then+marshalled to your type represeting the source data, via the FromJSON+typeclass.++The above example was actually insufficient; when working with+@aeson@ you need to fix the type so it knows what FromJSON instance+to use. Let's say you're getting Person objects, then it would be++>     v <- get "http://api.example.com/v1/person/461" jsonHandler :: IO Person++assuming your Person type had a FromJSON instance, of course.++/Note/++This function parses a single top level JSON object or array, which+is all you're supposed to get if it's a valid document. People do+all kinds of crazy things though, so beware. Also, this function (like the+"concatHander" convenience) loads the entire response into memory; it's+not /streaming/; if you're receiving a document which is (say) a very+long array of objects then you may want to implement your own+handler function, perhaps using "Streams.parserToInputStream" and+the 'Data.Aeson.Parser' combinators directly — with a result type of+InputStream Value, perhaps — by which you could then iterate over+the Values one at a time in constant space.+-}++{-+    This looks simple. It wasn't. The types involved are rediculous to+    disentangle. The biggest problem is that the Parser type used in+    [aeson] is *NOT* the Parser type from [attoparsec]. But the parsing+    function `json` and `json` from Aeson use the attoparsec Parser even+    though the rest of the top level page is all about Aeson's parser as+    used in FromJSON!++    Anyway, `json` and `json'` are [attoparsec] Parser [aeson] Value; we+    run that using the [io-streams] convenience function+    `parseFromStream` which gets us a Value which is the intermediate+    abstract syntax tree for a  JSON document. Then (and this was hard+    to find) to work with that in terms of the FromJSON typeclass, you+    use the `fromJSON` function which has type (FromJSON α => Value ->+    Result α). Then finally, pull the result out of it. Why in Bog's+    name this wasn't just Either I'll never know.+-}+jsonHandler ::+    (FromJSON α) =>+    Response ->+    InputStream ByteString ->+    IO α+jsonHandler _ i = do+    v <- Streams.parseFromStream json' i -- Value+    let r = fromJSON v -- Result+    case r of+        (Success a) -> return a+        (Error str) -> error str
+ lib/Network/Http/ResponseParser.hs view
@@ -0,0 +1,273 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2021 Athae Eredh Siniath and Others+--+-- The code in this file, and the program it is a part of, is+-- made available to you by its authors as open source software:+-- you can redistribute it and/or modify it under the terms of+-- the BSD licence.+--+-- Significant portions of this file were written while studying+-- the HTTP request parser implementation in the Snap Framework;+-- snap-core's src/Snap/Internal/Parsing.hs and snap-server's+-- src/Snap/Internal/Http/Parser.hs, and various utility functions+-- have been cloned from there.+--+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK hide, not-home #-}++module Network.Http.ResponseParser (+    readResponseHeader,+    readResponseBody,+    UnexpectedCompression (..),+    -- for testing+    readDecimal,+) where++import Prelude hiding (take, takeWhile)++import Control.Exception (Exception, throwIO)+import Control.Monad (void)+import Control.Monad.IO.Class (liftIO)+import Data.Attoparsec.ByteString.Char8+import Data.Bits (Bits (..))+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as S+import Data.CaseInsensitive (mk)+import Data.Char (ord)+import Data.Int (Int64)+import Data.Typeable (Typeable)+import System.IO.Streams (Generator, InputStream)+import qualified System.IO.Streams as Streams+import qualified System.IO.Streams.Attoparsec as Streams++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative+#endif++import Network.Http.Internal+import Network.Http.Utilities++{-+    The chunk size coming down from the server is somewhat arbitrary;+    it's really just an indication of how many bytes need to be read+    before the next size marker or end marker - neither of which has+    anything to do with streaming on our side. Instead, we'll feed+    bytes into our InputStream at an appropriate intermediate size.+-}+__BITE_SIZE__ :: Int+__BITE_SIZE__ = 32 * 1024++{-+    Process the reply from the server up to the end of the headers as+    deliniated by a blank line.+-}+readResponseHeader :: InputStream ByteString -> IO Response+readResponseHeader i = do+    (sc, sm) <- Streams.parseFromStream parseStatusLine i++    hs <- readHeaderFields i++    let h = buildHeaders hs+    let te = case lookupHeader h "Transfer-Encoding" of+            Just x' ->+                if mk x' == "chunked"+                    then Chunked+                    else None+            Nothing -> None++    let ce = case lookupHeader h "Content-Encoding" of+            Just x' ->+                if mk x' == "gzip"+                    then Gzip+                    else Identity+            Nothing -> Identity++    let nm = case lookupHeader h "Content-Length" of+            Just x' -> Just (readDecimal x' :: Int64)+            Nothing -> case sc of+                204 -> Just 0+                304 -> Just 0+                100 -> Just 0+                _ -> Nothing++    return+        Response+            { pStatusCode = sc+            , pStatusMsg = sm+            , pTransferEncoding = te+            , pContentEncoding = ce+            , pContentLength = nm+            , pHeaders = h+            }++parseStatusLine :: Parser (Int, ByteString)+parseStatusLine = do+    sc <- string "HTTP/1." *> satisfy version *> char ' ' *> decimal <* char ' '+    sm <- takeTill (== '\r') <* crlf+    return (sc, sm)+  where+    version c = c == '1' || c == '0'++crlf :: Parser ByteString+crlf = string "\r\n"++---------------------------------------------------------------------++{-+    Switch on the encoding and compression headers, wrapping the raw+    InputStream to present the entity body's actual bytes.+-}+readResponseBody :: Response -> InputStream ByteString -> IO (InputStream ByteString)+readResponseBody p i1 = do+    i2 <- case t of+        None -> case l of+            Just n -> readFixedLengthBody i1 n+            Nothing -> readUnlimitedBody i1+        Chunked -> readChunkedBody i1++    i3 <- case c of+        Identity -> return i2+        Gzip -> readCompressedBody i2+        Deflate -> throwIO (UnexpectedCompression $ show c)++    return i3+  where+    t = pTransferEncoding p+    c = pContentEncoding p+    l = pContentLength p++readDecimal :: (Enum α, Num α, Bits α) => ByteString -> α+readDecimal str' =+    S.foldl' f 0 x'+  where+    f !cnt !i = cnt * 10 + digitToInt i++    x' = head $ S.words str'++    {-# INLINE digitToInt #-}+    digitToInt :: (Enum α, Num α, Bits α) => Char -> α+    digitToInt c+        | c >= '0' && c <= '9' = toEnum $! ord c - ord '0'+        | otherwise = error $ "'" ++ [c] ++ "' is not an ascii digit"+{-# INLINE readDecimal #-}++data UnexpectedCompression = UnexpectedCompression String+    deriving (Typeable, Show)++instance Exception UnexpectedCompression++---------------------------------------------------------------------++{-+    Process a response body in chunked transfer encoding, taking the+    resultant bytes and reproducing them as an InputStream+-}+readChunkedBody :: InputStream ByteString -> IO (InputStream ByteString)+readChunkedBody i1 = do+    i2 <- Streams.fromGenerator (consumeChunks i1)+    return i2++{-+    For a response body in chunked transfer encoding, iterate over+    the individual chunks, reading the size parameter, then+    looping over that chunk in bites of at most __BYTE_SIZE__,+    yielding them to the receiveResponse InputStream accordingly.+-}+consumeChunks :: InputStream ByteString -> Generator ByteString ()+consumeChunks i1 = do+    !n <- parseSize++    if n > 0+        then do+            -- read one or more bites, then loop to next chunk+            go n+            skipCRLF+            consumeChunks i1+        else do+            -- skip "trailers" and consume final CRLF+            skipEnd+  where+    go 0 = return ()+    go !n = do+        (!x', !r) <- liftIO $ readN n i1+        Streams.yield x'+        go r++    parseSize = do+        n <- liftIO $ Streams.parseFromStream transferChunkSize i1+        return n++    skipEnd = do+        liftIO $ do+            _ <- readHeaderFields i1+            return ()++    skipCRLF = do+        liftIO $ do+            _ <- Streams.parseFromStream crlf i1+            return ()++{-+    Read the specified number of bytes up to a maximum of __BITE_SIZE__,+    returning a resultant ByteString and the number of bytes remaining.+-}++readN :: Int -> InputStream ByteString -> IO (ByteString, Int)+readN n i1 = do+    !x' <- Streams.readExactly p i1+    return (x', r)+  where+    !d = n - size++    !p =+        if d > 0+            then size+            else n++    !r =+        if d > 0+            then d+            else 0++    size = __BITE_SIZE__++transferChunkSize :: Parser (Int)+transferChunkSize = do+    !n <- hexadecimal+    void (takeTill (== '\r'))+    void crlf+    return n++---------------------------------------------------------------------++{-+    This has the rather crucial side effect of terminating the stream+    after the requested number of bytes. Otherwise, code handling+    responses waits on more input until an HTTP timeout occurs.+-}+readFixedLengthBody :: InputStream ByteString -> Int64 -> IO (InputStream ByteString)+readFixedLengthBody i1 n = do+    i2 <- Streams.takeBytes n i1+    return i2++{-+    On the other hand, there is the (predominently HTTP/1.0) case+    where there is no content length sent and no chunking, with the+    result that only the connection closing marks the end of the+    response body.+-}+readUnlimitedBody :: InputStream ByteString -> IO (InputStream ByteString)+readUnlimitedBody i1 = do+    return i1++---------------------------------------------------------------------++readCompressedBody :: InputStream ByteString -> IO (InputStream ByteString)+readCompressedBody i1 = do+    i2 <- Streams.gunzip i1+    return i2
+ lib/Network/Http/Utilities.hs view
@@ -0,0 +1,269 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2021 Athae Eredh Siniath and Others+--+-- The code in this file, and the program it is a part of, is+-- made available to you by its authors as open source software:+-- you can redistribute it and/or modify it under the terms of+-- the BSD licence.+--+-- This file is essentially a clone of Snap.Internal.Parsing,+-- the HTTP request parser implementation in the Snap Framework;+-- snap-core's src/Snap/Internal/Parsing.hs and snap-server's+-- src/Snap/Internal/Http/Parser.hs, copied here to specialize+-- it to Response parsing. This code replaces the attoparsec+-- based implementation formerly in ResponseParser, but is+-- kept separate to aid syncing changes from snap-core as they+-- become available.+--+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE UnboxedTuples #-}+{-# OPTIONS_HADDOCK hide, not-home #-}++module Network.Http.Utilities (+    readResponseLine,+    readHeaderFields,+) where++------------------------------------------------------------------------------+import Control.Exception (throwIO)+import Control.Monad (when)+import Data.Bits+import qualified Data.ByteString.Char8 as S+import Data.ByteString.Internal (ByteString, w2c)+import qualified Data.ByteString.Unsafe as S+import Data.Char hiding (digitToInt, isDigit, isSpace)+import GHC.Exts (Int (..), Int#, (+#))+import System.IO.Streams (InputStream)+import qualified System.IO.Streams as Streams+import Prelude hiding (head, take, takeWhile)++----------------------------------------------------------------------------++import Network.Http.Types++------------------------------------------------------------------------------++{-+    This is vestigial; originally it was the Request parsing+    code in Snap. Keeping it here until we can use if for+    response parsing.+-}+parseRequest :: InputStream ByteString -> IO (Maybe Request)+parseRequest input = do+    eof <- Streams.atEOF input+    if eof+        then return Nothing+        else do+            line <- readResponseLine input+            let (!mStr, !s) = bSp line+            let (!uri, !vStr) = bSp s+            let !version = pVer vStr :: (Int, Int)++            --      hdrs    <- readHeaderFields input+            return $! Nothing+  where+    pVer s =+        if "HTTP/" `S.isPrefixOf` s+            then pVers (S.unsafeDrop 5 s)+            else (1, 0)++    bSp = splitCh ' '++    pVers s = (c, d)+      where+        (!a, !b) = splitCh '.' s+        !c = unsafeFromNat a+        !d = unsafeFromNat b++{-+    Read a single line of an HTTP response.+-}+readResponseLine :: InputStream ByteString -> IO ByteString+readResponseLine input = go []+  where+    throwNoCRLF =+        throwIO $+            HttpParseException "parse error: expected line ending in crlf"++    throwBadCRLF =+        throwIO $+            HttpParseException "parse error: got cr without subsequent lf"++    go !l = do+        !mb <- Streams.read input+        !s <- maybe throwNoCRLF return mb++        case findCRLF s of+            FoundCRLF idx# -> foundCRLF l s idx#+            NoCR -> noCRLF l s+            LastIsCR idx# -> lastIsCR l s idx#+            _ -> throwBadCRLF++    foundCRLF l s idx# = do+        let !i1 = (I# idx#)+        let !i2 = (I# (idx# +# 2#))+        let !a = S.unsafeTake i1 s+        when (i2 < S.length s) $ do+            let !b = S.unsafeDrop i2 s+            Streams.unRead b input++        -- Optimize for the common case: dl is almost always "id"+        let !out = if null l then a else S.concat (reverse (a : l))+        return out++    noCRLF l s = go (s : l)++    lastIsCR l s idx# = do+        !t <- Streams.read input >>= maybe throwNoCRLF return+        if S.null t+            then lastIsCR l s idx#+            else do+                let !c = S.unsafeHead t+                if c /= 10+                    then throwBadCRLF+                    else do+                        let !a = S.unsafeTake (I# idx#) s+                        let !b = S.unsafeDrop 1 t+                        when (not $ S.null b) $ Streams.unRead b input+                        let !out = if null l then a else S.concat (reverse (a : l))+                        return out++------------------------------------------------------------------------------+data CS+    = FoundCRLF !Int#+    | NoCR+    | LastIsCR !Int#+    | BadCR++------------------------------------------------------------------------------+findCRLF :: ByteString -> CS+findCRLF b =+    case S.elemIndex '\r' b of+        Nothing -> NoCR+        Just !i@(I# i#) ->+            let !i' = i + 1+             in if i' < S.length b+                    then+                        if S.unsafeIndex b i' == 10+                            then FoundCRLF i#+                            else BadCR+                    else LastIsCR i#+{-# INLINE findCRLF #-}++------------------------------------------------------------------------------+splitCh :: Char -> ByteString -> (ByteString, ByteString)+splitCh !c !s = maybe (s, S.empty) f (S.elemIndex c s)+  where+    f !i =+        let !a = S.unsafeTake i s+            !b = S.unsafeDrop (i + 1) s+         in (a, b)+{-# INLINE splitCh #-}++------------------------------------------------------------------------------+breakCh :: Char -> ByteString -> (ByteString, ByteString)+breakCh !c !s = maybe (s, S.empty) f (S.elemIndex c s)+  where+    f !i =+        let !a = S.unsafeTake i s+            !b = S.unsafeDrop i s+         in (a, b)+{-# INLINE breakCh #-}++------------------------------------------------------------------------------+splitHeader :: ByteString -> (ByteString, ByteString)+splitHeader !s = maybe (s, S.empty) f (S.elemIndex ':' s)+  where+    l = S.length s++    f i =+        let !a = S.unsafeTake i s+         in (a, skipSp (i + 1))++    skipSp !i+        | i >= l = S.empty+        | otherwise =+            let c = S.unsafeIndex s i+             in if isLWS $ w2c c+                    then skipSp $ i + 1+                    else S.unsafeDrop i s+{-# INLINE splitHeader #-}++------------------------------------------------------------------------------+isLWS :: Char -> Bool+isLWS c = c == ' ' || c == '\t'+{-# INLINE isLWS #-}++------------------------------------------------------------------------------++{-+    Read the remainder of the response message's header section,+    parsing into key/value pairs. Note that this function terminates+    when it hits the "blank" line (ie, CRLF CRLF pair), which it+    consumes.+-}+readHeaderFields :: InputStream ByteString -> IO [(ByteString, ByteString)]+readHeaderFields input = do+    f <- go id+    return $! f []+  where+    go !dlistSoFar = do+        line <- readResponseLine input+        if S.null line+            then return dlistSoFar+            else do+                let (!k, !v) = splitHeader line+                vf <- pCont id+                let vs = vf []+                let !v' = if null vs then v else S.concat (v : vs)+                let !t = (k, v')+                go (dlistSoFar . (t :))+      where+        trimBegin = S.dropWhile isLWS++        pCont !dlist = do+            mbS <- Streams.peek input+            maybe+                (return dlist)+                ( \s ->+                    if S.null s+                        then Streams.read input >> pCont dlist+                        else+                            if isLWS $ w2c $ S.unsafeHead s+                                then procCont dlist+                                else return dlist+                )+                mbS++        procCont !dlist = do+            line <- readResponseLine input+            let !t = trimBegin line+            pCont (dlist . (" " :) . (t :))++-----------------------+-- utility functions --+-----------------------++------------------------------------------------------------------------------++-- | Note: only works for nonnegative naturals+unsafeFromNat :: (Enum a, Num a, Bits a) => ByteString -> a+unsafeFromNat = S.foldl' f 0+  where+    zero = ord '0'+    f !cnt !i = cnt * 10 + toEnum (digitToInt i)++    digitToInt c =+        if d >= 0 && d <= 9+            then d+            else error $ "bad digit: '" ++ [c] ++ "'"+      where+        !d = ord c - zero+{-# INLINE unsafeFromNat #-}
− src/Network/Http/Client.hs
@@ -1,176 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.-----{-# LANGUAGE OverloadedStrings #-}-{-# OPTIONS -fno-warn-orphans #-}---- |--- Maintainer: Andrew Cowie--- Stability: Experimental------ /Overview/------ A simple HTTP client library, using the Snap Framework's @io-streams@--- library to handle the streaming I\/O. The @http-streams@ API is designed--- for ease of use when querying web services and dealing with the result.------ Given:------ > {-# LANGUAGE OverloadedStrings #-}--- >--- > import System.IO.Streams (InputStream, OutputStream, stdout)--- > import qualified System.IO.Streams as Streams--- > import qualified Data.ByteString as S------ and this library:------ > import Network.Http.Client------ the underlying API is straight-forward. In particular, constructing the--- 'Request' to send is quick and to the point:------ @--- main :: IO ()--- main = do--- \    c <- 'openConnection' \"www.example.com\" 80------ \     q <- 'buildRequest' $ do---         'http' GET \"\/\"---         'setAccept' \"text/html\"------ \     'sendRequest' c q 'emptyBody'------ \     `receiveResponse` c (\\p i -> do---         xm <- Streams.read i---         case xm of---             Just x    -> S.putStr x---             Nothing   -> \"\")------ \     'closeConnection' c--- @------ which would print the first chunk of the response back from the--- server. Obviously in real usage you'll do something more interesting--- with the 'Response' in the handler function, and consume the entire--- response body from the InputStream ByteString.------ Because this is all happening in 'IO' (the defining feature of--- @io-streams@!), you can ensure resource cleanup on normal or--- abnormal termination by using @Control.Exception@'s standard--- 'Control.Exception.bracket' function; see 'closeConnection' for an--- example. For the common case we have a utility function which--- wraps @bracket@ for you:------ @--- foo :: IO ByteString--- foo = 'withConnection' ('openConnection' \"www.example.com\" 80) doStuff------ \ doStuff :: Connection -> IO ByteString--- @------ There are also a set of convenience APIs that do just that, along with--- the tedious bits like parsing URLs. For example, to do an HTTP GET and--- stream the response body to stdout, you can simply do:------ @---     'get' \"http:\/\/www.example.com\/file.txt\" (\\p i -> Streams.connect i stdout)--- @------ which on the one hand is \"easy\" while on the other exposes the the--- 'Response' and InputStream for you to read from. Of course, messing--- around with URLs is all a bit inefficient, so if you already have e.g.--- hostname and path, or if you need more control over the request being--- created, then the underlying @http-streams@ API is simple enough to use--- directly.----module Network.Http.Client (-    -- * Connecting to server-    Hostname,-    Port,-    Connection,-    openConnection,--    -- * Building Requests-    -- | You setup a request using the RequestBuilder monad, and-    -- get the resultant Request object by running 'buildRequest'. The-    -- first call doesn't have to be to 'http', but it looks better when-    -- it is, don't you think?-    Method(..),-    RequestBuilder,-    buildRequest,-    http,-    setHostname,-    setAccept,-    setAccept',-    setAuthorizationBasic,-    ContentType,-    setContentType,-    setContentLength,-    setExpectContinue,-    setHeader,--    -- * Sending HTTP request-    Request,-    Response,-    Headers,-    getHostname,-    sendRequest,-    emptyBody,-    fileBody,-    inputStreamBody,-    encodedFormBody,--    -- * Processing HTTP response-    receiveResponse,-    receiveResponseRaw,-    StatusCode,-    getStatusCode,-    getStatusMessage,-    getHeader,-    debugHandler,-    concatHandler,-    concatHandler',-    jsonHandler,--    -- * Resource cleanup-    closeConnection,-    withConnection,--    -- * Convenience APIs-    -- | Some simple functions for making requests with useful defaults.-    -- There's no @head@ function for the usual reason of needing to-    -- avoid collision with @Prelude@.-    ---    -- These convenience functions work with @http@ and @https@, but-    --  note that if you retrieve an @https@ URL, you /must/ wrap your-    -- @main@ function with 'OpenSSL.withOpenSSL' to initialize the-    -- native openssl library code.-    ---    URL,-    get,-    post,-    postForm,-    put,--    -- * Secure connections-    openConnectionSSL,-    baselineContextSSL,-    modifyContextSSL,-    establishConnection,--    -- * Testing support-    makeConnection-) where--import Network.Http.Connection-import Network.Http.Inconvenience-import Network.Http.RequestBuilder-import Network.Http.Types
− src/Network/Http/Connection.hs
@@ -1,585 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.-----{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DoAndIfThenElse    #-}-{-# LANGUAGE OverloadedStrings  #-}--module Network.Http.Connection (-    Hostname,-    Port,-    Connection(..),-        -- constructors only for testing-    makeConnection,-    withConnection,-    openConnection,-    openConnectionSSL,-    closeConnection,-    getHostname,-    sendRequest,-    receiveResponse,-    receiveResponseRaw,-    emptyBody,-    fileBody,-    inputStreamBody,-    debugHandler,-    concatHandler-) where--import Blaze.ByteString.Builder (Builder)-import qualified Blaze.ByteString.Builder as Builder (flush, fromByteString,-                                                      toByteString)-import qualified Blaze.ByteString.Builder.HTTP as Builder (chunkedTransferEncoding, chunkedTransferTerminator)-import Control.Exception (bracket)-import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as S-import Data.Monoid (mappend, mempty)-import Data.Word (Word16)-import Network.Socket-import OpenSSL.Session (SSL, SSLContext)-import qualified OpenSSL.Session as SSL-import System.IO.Streams (InputStream, OutputStream, stdout)-import qualified System.IO.Streams as Streams-import qualified System.IO.Streams.SSL as Streams hiding (connect)--import Network.Http.ResponseParser-import Network.Http.Types--{--    This is a String because that's what the uri package works in. There-    was a fairly detailed disucssion on haskell-cafe about this, with-    the conclusion that URLs are composed of characters, not octets.--}--type Hostname = ByteString--type Port = Word16---- | A connection to a web server.----data Connection-    = Connection {-        cHost  :: ByteString,-            -- ^ will be used as the Host: header in the HTTP request.-        cClose :: IO (),-            -- ^ called when the connection should be closed.-        cOut   :: OutputStream Builder,-        cIn    :: InputStream ByteString-    }--instance Show Connection where-    show c =    {-# SCC "Connection.show" #-}-        concat-           ["Host: ",-             S.unpack $ cHost c,-             "\n"]-------- | Create a raw Connection object from the given parts. This is--- primarily of use when teseting, for example:------ > fakeConnection :: IO Connection--- > fakeConnection = do--- >     o  <- Streams.nullOutput--- >     i  <- Streams.nullInput--- >     c  <- makeConnection "www.example.com" (return()) o i--- >     return c------ is an idiom we use frequently in testing and benchmarking, usually--- replacing the InputStream with something like:------ >     x' <- S.readFile "properly-formatted-response.txt"--- >     i  <- Streams.fromByteString x'------ If you're going to do that, keep in mind that you /must/ have CR-LF--- pairs after each header line and between the header and body to--- be compliant with the HTTP protocol; otherwise, parsers will--- reject your message.----makeConnection-    :: ByteString-    -- ^ will be used as the @Host:@ header in the HTTP request.-    -> IO ()-    -- ^ an action to be called when the connection is terminated.-    -> OutputStream ByteString-    -- ^ write end of the HTTP client-server connection.-    -> InputStream ByteString-    -- ^ read end of the HTTP client-server connection.-    -> IO Connection-makeConnection h c o1 i = do-    o2 <- Streams.builderStream o1-    return $! Connection h c o2 i-------- | Given an @IO@ action producing a 'Connection', and a computation--- that needs one, runs the computation, cleaning up the--- @Connection@ afterwards.------ >     x <- withConnection (openConnection "s3.example.com" 80) $ (\c -> do--- >         q <- buildRequest $ do--- >             http GET "/bucket42/object/149"--- >         sendRequest c q emptyBody--- >         ...--- >         return "blah")------ which can make the code making an HTTP request a lot more--- straight-forward.------ Wraps @Control.Exception@'s 'Control.Exception.bracket'.----withConnection :: IO Connection -> (Connection -> IO γ) -> IO γ-withConnection mkC =-    bracket mkC closeConnection-------- | In order to make a request you first establish the TCP--- connection to the server over which to send it.------ Ordinarily you would supply the host part of the URL here and it will--- be used as the value of the HTTP 1.1 @Host:@ field. However, you can--- specify any server name or IP addresss and set the @Host:@ value--- later with 'Network.Http.Client.setHostname' when building the--- request.------ Usage is as follows:------ >     c <- openConnection "localhost" 80--- >     ...--- >     closeConnection c------ More likely, you'll use 'withConnection' to wrap the call in order--- to ensure finalization.------ HTTP pipelining is supported; you can reuse the connection to a--- web server, but it's up to you to ensure you match the number of--- requests sent to the number of responses read, and to process those--- responses in order. This is all assuming that the /server/ supports--- pipelining; be warned that not all do. Web browsers go to--- extraordinary lengths to probe this; you probably only want to do--- pipelining under controlled conditions. Otherwise just open a new--- connection for subsequent requests.----openConnection :: Hostname -> Port -> IO Connection-openConnection h1' p = do-    is <- getAddrInfo (Just hints) (Just h1) (Just $ show p)-    let addr = head is-    let a = addrAddress addr-    s <- socket (addrFamily addr) Stream defaultProtocol--    connect s a-    (i,o1) <- Streams.socketToStreams s--    o2 <- Streams.builderStream o1--    return Connection {-        cHost  = h2',-        cClose = close s,-        cOut   = o2,-        cIn    = i-    }-  where-    hints = defaultHints {addrFlags = [AI_ADDRCONFIG, AI_NUMERICSERV]}-    h2' = if p == 80-        then h1'-        else S.concat [ h1', ":", S.pack $ show p ]-    h1  = S.unpack h1'------- | Open a secure connection to a web server.------ You need to wrap this (and subsequent code using this connection)--- within a call to 'OpenSSL.withOpenSSL':------ > import OpenSSL (withOpenSSL)--- >--- > main :: IO ()--- > main = withOpenSSL $ do--- >     ctx <- baselineContextSSL--- >     c <- openConnectionSSL ctx "api.github.com" 443--- >     ...--- >     closeConnection c------ If you want to tune the parameters used in making SSL connections,--- manually specify certificates, etc, then setup your own context:------ > import OpenSSL.Session (SSLContext)--- > import qualified OpenSSL.Session as SSL--- >--- >     ...--- >     ctx <- SSL.context--- >     ...------ See "OpenSSL.Session".------ Crypto is as provided by the system @openssl@ library, as wrapped--- by the @HsOpenSSL@ package and @openssl-streams@.----openConnectionSSL :: SSLContext -> Hostname -> Port -> IO Connection-openConnectionSSL ctx h1' p = do-    is <- getAddrInfo Nothing (Just h1) (Just $ show p)--    let a = addrAddress $ head is-        f = addrFamily $ head is-    s <- socket f Stream defaultProtocol--    connect s a--    ssl <- SSL.connection ctx s-    SSL.connect ssl--    (i,o1) <- Streams.sslToStreams ssl--    o2 <- Streams.builderStream o1--    return Connection {-        cHost  = h2',-        cClose = closeSSL s ssl,-        cOut   = o2,-        cIn    = i-    }-  where-    h2' :: ByteString-    h2' = if p == 443-        then h1'-        else S.concat [ h1', ":", S.pack $ show p ]-    h1  = S.unpack h1'--closeSSL :: Socket -> SSL -> IO ()-closeSSL s ssl = do-    SSL.shutdown ssl SSL.Unidirectional-    close s------- | Having composed a 'Request' object with the headers and metadata for--- this connection, you can now send the request to the server, along--- with the entity body, if there is one. For the rather common case of--- HTTP requests like 'GET' that don't send data, use 'emptyBody' as the--- output stream:------ >     sendRequest c q emptyBody------ For 'PUT' and 'POST' requests, you can use 'fileBody' or--- 'inputStreamBody' to send content to the server, or you can work with--- the @io-streams@ API directly:------ >     sendRequest c q (\o ->--- >         Streams.write (Just (Builder.fromString "Hello World\n")) o)----{--    I would like to enforce the constraints on the Empty and Static-    cases shown here, but those functions take OutputStream ByteString,-    and we are of course working in OutputStream Builder by that point.--}-sendRequest :: Connection -> Request -> (OutputStream Builder -> IO α) -> IO α-sendRequest c q handler = do-    -- write the headers--    Streams.write (Just msg) o2--    -- deal with the expect-continue mess--    e2 <- case t of-        Normal -> do-            return e--        Continue -> do-            Streams.write (Just Builder.flush) o2--            p  <- readResponseHeader i--            case getStatusCode p of-                100 -> do-                        -- ok to send-                        return e-                _   -> do-                        -- put the response back-                        Streams.unRead (rsp p) i-                        return Empty--    -- write the body, if there is one--    x <- case e2 of-        Empty -> do-            o3 <- Streams.nullOutput-            y <- handler o3-            return y--        Chunking    -> do-            o3 <- Streams.contramap Builder.chunkedTransferEncoding o2-            y  <- handler o3-            Streams.write (Just Builder.chunkedTransferTerminator) o2-            return y--        (Static _) -> do---          o3 <- Streams.giveBytes (fromIntegral n :: Int64) o2-            y  <- handler o2-            return y---    -- push the stream out by flushing the output buffers--    Streams.write (Just Builder.flush) o2--    return x--  where-    o2 = cOut c-    e = qBody q-    t = qExpect q-    msg = composeRequestBytes q h'-    h' = cHost c-    i = cIn c-    rsp p = Builder.toByteString $ composeResponseBytes p-------- | Get the virtual hostname that will be used as the @Host:@ header in--- the HTTP 1.1 request. Per RFC 2616 § 14.23, this will be of the form--- @hostname:port@ if the port number is other than the default, ie 80--- for HTTP.----getHostname :: Connection -> Request -> ByteString-getHostname c q =-    case qHost q of-        Just h' -> h'-        Nothing -> cHost c-------- | Handle the response coming back from the server. This function--- hands control to a handler function you supply, passing you the--- 'Response' object with the response headers and an 'InputStream'--- containing the entity body.------ For example, if you just wanted to print the first chunk of the--- content from the server:------ >     receiveResponse c (\p i -> do--- >         m <- Streams.read i--- >         case m of--- >             Just bytes -> putStr bytes--- >             Nothing    -> return ())------ Obviously, you can do more sophisticated things with the--- 'InputStream', which is the whole point of having an @io-streams@--- based HTTP client library.------ The final value from the handler function is the return value of--- @receiveResponse@, if you need it.----{--    The reponse body coming from the server MUST be fully read, even-    if (especially if) the users's handler doesn't consume it all.-    This is necessary to maintain the HTTP protocol invariants;-    otherwise pipelining would not work. It's not entirely clear-    *which* InputStream is being drained here; the underlying-    InputStream ByteString in Connection remains unconsumed beyond the-    threshold of the current response, which is exactly what we need.--}-receiveResponse :: Connection -> (Response -> InputStream ByteString -> IO β) -> IO β-receiveResponse c handler = do-    p  <- readResponseHeader i-    i' <- readResponseBody p i--    x  <- handler p i'--    Streams.skipToEof i'--    return x-  where-    i = cIn c------- | This is a specialized variant of 'receiveResponse' that /explicitly/ does--- not handle the content encoding of the response body stream (it will not--- decompress anything). Unless you really want the raw gzipped content coming--- down from the server, use @receiveResponse@.----{--    See notes at receiveResponse.--}-receiveResponseRaw :: Connection -> (Response -> InputStream ByteString -> IO β) -> IO β-receiveResponseRaw c handler = do-    p  <- readResponseHeader i-    let p' = p {-        pContentEncoding = Identity-    }--    i' <- readResponseBody p' i--    x  <- handler p i'--    Streams.skipToEof i'--    return x-  where-    i = cIn c-------- | Use this for the common case of the HTTP methods that only send--- headers and which have no entity body, i.e. 'GET' requests.----emptyBody :: OutputStream Builder -> IO ()-emptyBody _ = return ()-------- | Specify a local file to be sent to the server as the body of the--- request.------ You use this partially applied:------ >     sendRequest c q (fileBody "/etc/passwd")------ Note that the type of @(fileBody \"\/path\/to\/file\")@ is just what--- you need for the third argument to 'sendRequest', namely------ >>> :t filePath "hello.txt"--- :: OutputStream Builder -> IO ()----{--    Relies on Streams.withFileAsInput generating (very) large chunks [which it-    does]. A more efficient way to do this would be interesting.--}-fileBody :: FilePath -> OutputStream Builder -> IO ()-fileBody p o = do-    Streams.withFileAsInput p (\i -> inputStreamBody i o)-------- | Read from a pre-existing 'InputStream' and pipe that through to the--- connection to the server. This is useful in the general case where--- something else has handed you stream to read from and you want to use--- it as the entity body for the request.------ You use this partially applied:------ >     i <- getStreamFromVault                    -- magic, clearly--- >     sendRequest c q (inputStreamBody i)------ This function maps "Builder.fromByteString" over the input, which will--- be efficient if the ByteString chunks are large.----{--    Note that this has to be 'supply' and not 'connect' as we do not-    want the end of stream to prematurely terminate the chunked encoding-    pipeline!--}-inputStreamBody :: InputStream ByteString -> OutputStream Builder -> IO ()-inputStreamBody i1 o = do-    i2 <- Streams.map Builder.fromByteString i1-    Streams.supply i2 o-------- | Print the response headers and response body to @stdout@. You can--- use this with 'receiveResponse' or one of the convenience functions--- when testing. For example, doing:------ >     c <- openConnection "kernel.operationaldynamics.com" 58080--- >--- >     q <- buildRequest $ do--- >         http GET "/time"--- >--- >     sendRequest c q emptyBody--- >--- >     receiveResponse c debugHandler------ would print out:------ > HTTP/1.1 200 OK--- > Transfer-Encoding: chunked--- > Content-Type: text/plain--- > Vary: Accept-Encoding--- > Server: Snap/0.9.2.4--- > Content-Encoding: gzip--- > Date: Mon, 21 Jan 2013 06:13:37 GMT--- >--- > Mon 21 Jan 13, 06:13:37.303Z------ or thereabouts.----debugHandler :: Response -> InputStream ByteString -> IO ()-debugHandler p i = do-    S.putStr $ S.filter (/= '\r') $ Builder.toByteString $ composeResponseBytes p-    Streams.connect i stdout-------- | Sometimes you just want the entire response body as a single blob.--- This function concatonates all the bytes from the response into a--- ByteString. If using the main @http-streams@ API, you would use it--- as follows:------ >    ...--- >    x' <- receiveResponse c concatHandler--- >    ...------ The methods in the convenience API all take a function to handle the--- response; this function is passed directly to the 'receiveResponse'--- call underlying the request. Thus this utility function can be used--- for 'get' as well:------ >    x' <- get "http://www.example.com/document.txt" concatHandler------ Either way, the usual caveats about allocating a--- single object from streaming I/O apply: do not use this if you are--- not absolutely certain that the response body will fit in a--- reasonable amount of memory.------ Note that this function makes no discrimination based on the--- response's HTTP status code. You're almost certainly better off--- writing your own handler function.----{--    I'd welcome a better name for this function.--}-concatHandler :: Response -> InputStream ByteString -> IO ByteString-concatHandler _ i1 = do-    i2 <- Streams.map Builder.fromByteString i1-    x <- Streams.fold mappend mempty i2-    return $ Builder.toByteString x-------- | Shutdown the connection. You need to call this release the--- underlying socket file descriptor and related network resources. To--- do so reliably, use this in conjunction with 'openConnection' in a--- call to 'Control.Exception.bracket':------ > ----- > -- Make connection, cleaning up afterward--- > ----- >--- > foo :: IO ByteString--- > foo = bracket--- >    (openConnection "localhost" 80)--- >    (closeConnection)--- >    (doStuff)--- >--- > ----- > -- Actually use Connection to send Request and receive Response--- > ----- >--- > doStuff :: Connection -> IO ByteString------ or, just use 'withConnection'.------ While returning a ByteString is probably the most common use case,--- you could conceivably do more processing of the response in 'doStuff'--- and have it and 'foo' return a different type.----closeConnection :: Connection -> IO ()-closeConnection c = cClose c
− src/Network/Http/Inconvenience.hs
@@ -1,573 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.-----{-# LANGUAGE BangPatterns       #-}-{-# LANGUAGE CPP                #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE MagicHash          #-}-{-# LANGUAGE OverloadedStrings  #-}-{-# OPTIONS -fno-warn-orphans  #-}--module Network.Http.Inconvenience (-    URL,-    modifyContextSSL,-    establishConnection,-    get,-    post,-    postForm,-    encodedFormBody,-    put,-    baselineContextSSL,-    concatHandler',-    jsonHandler,--    -- for testing-    TooManyRedirects(..),-    HttpClientError(..)-) where--import Blaze.ByteString.Builder (Builder)-import qualified Blaze.ByteString.Builder as Builder (fromByteString,-                                                      fromWord8, toByteString)-import qualified Blaze.ByteString.Builder.Char8 as Builder (fromString)-import Control.Exception (Exception, bracket, throw)-import Data.Aeson (FromJSON, Result (..), fromJSON, json')-import Data.Bits (Bits (..))-import Data.ByteString.Char8 (ByteString)-import qualified Data.ByteString.Char8 as S-import Data.ByteString.Internal (c2w, w2c)-import Data.Char (intToDigit, isAlphaNum)-import Data.HashSet (HashSet)-import qualified Data.HashSet as HashSet-import Data.IORef (IORef, newIORef, readIORef, writeIORef)-import Data.List (intersperse)-import Data.Monoid (Monoid (..), mappend)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import Data.Typeable (Typeable)-import Data.Word (Word16)-import GHC.Exts-import GHC.Word (Word8 (..))-import Network.URI (URI (..), URIAuth (..), parseURI)-import OpenSSL.Session (SSLContext)-import qualified OpenSSL.Session as SSL-import System.IO.Streams (InputStream, OutputStream)-import qualified System.IO.Streams as Streams-import qualified System.IO.Streams.Attoparsec as Streams-import System.IO.Unsafe (unsafePerformIO)--import Network.Http.Connection-import Network.Http.RequestBuilder-import Network.Http.Types--#include "config.h"--type URL = ByteString--------------------------------------------------------------------------------------- | URL-escapes a string (see--- <http://tools.ietf.org/html/rfc2396.html#section-2.4>)----urlEncode :: ByteString -> URL-urlEncode = Builder.toByteString . urlEncodeBuilder-{-# INLINE urlEncode #-}-------- | URL-escapes a string (see--- <http://tools.ietf.org/html/rfc2396.html#section-2.4>) into a 'Builder'.----urlEncodeBuilder :: ByteString -> Builder-urlEncodeBuilder = go mempty-  where-    go !b !s = maybe b' esc (S.uncons y)-      where-        (x,y)     = S.span (flip HashSet.member urlEncodeTable) s-        b'        = b `mappend` Builder.fromByteString x-        esc (c,r) = let b'' = if c == ' '-                                then b' `mappend` Builder.fromWord8 (c2w '+')-                                else b' `mappend` hexd c-                    in go b'' r---hexd :: Char -> Builder-hexd c0 = Builder.fromWord8 (c2w '%') `mappend` Builder.fromWord8 hi-                                      `mappend` Builder.fromWord8 low-  where-    !c        = c2w c0-    toDigit   = c2w . intToDigit-    !low      = toDigit $ fromEnum $ c .&. 0xf-    !hi       = toDigit $ (c .&. 0xf0) `shiftr` 4--    shiftr (W8# a#) (I# b#) = I# (word2Int# (uncheckedShiftRL# a# b#))---urlEncodeTable :: HashSet Char-urlEncodeTable = HashSet.fromList $! filter f $! map w2c [0..255]-  where-    f c = isAlphaNum c || elem c "$-.!*'(),"-----------------------------------------------------------------------------------{--    The default SSLContext used by the convenience APIs in the http-streams-    library. This is a kludge, unsafe bad yada yada. The technique, however,-    was described on a Haskell Wiki page, so that makes it an officially-    supported kludge. The justification for doing this is a) the functions-    accessing this IORef are themselves all in the IO monad, and b) these-    contortions are necessary to allow the library to be used for http:// URLs-    *without* requiring the developer to do 'withOpenSSL'.--}-global :: IORef SSLContext-global = unsafePerformIO $ do-    ctx <- baselineContextSSL-    newIORef ctx-{-# NOINLINE global #-}------- | Modify the context being used to configure the SSL tunnel used by--- the convenience API functions to make @https://@ connections. The--- default is that setup by 'baselineContextSSL'.----modifyContextSSL :: (SSLContext -> IO SSLContext) -> IO ()-modifyContextSSL f = do-    ctx <- readIORef global-    ctx' <- f ctx-    writeIORef global ctx'------- | Given a URL, work out whether it is normal or secure, and then--- open the connection to the webserver including setting the--- appropriate default port if one was not specified in the URL. This--- is what powers the convenience API, but you may find it useful in--- composing your own similar functions.------ For example (on the assumption that your server behaves when given--- an absolute URI as the request path), this will open a connection--- to server @www.example.com@ port @443@ and request @/photo.jpg@:------ >     let url = "https://www.example.com/photo.jpg"--- >--- >     c <- establishConnection url--- >     q <- buildRequest $ do--- >         http GET url--- >     ...----establishConnection :: URL -> IO (Connection)-establishConnection r' = do-    establish u-  where-    u = parseURL r'-{-# INLINE establishConnection #-}--establish :: URI -> IO (Connection)-establish u =-    case scheme of-        "http:"  -> do-                        openConnection host port-        "https:" -> do-                        ctx <- readIORef global-                        openConnectionSSL ctx host ports-        _        -> error ("Unknown URI scheme " ++ scheme)-  where-    scheme = uriScheme u--    auth = case uriAuthority u of-        Just x  -> x-        Nothing -> URIAuth "" "localhost" ""--    host = S.pack (uriRegName auth)-    port = case uriPort auth of-        ""  -> 80-        _   -> read $ tail $ uriPort auth :: Word16-    ports = case uriPort auth of-        ""  -> 443-        _   -> read $ tail $ uriPort auth :: Word16-------- | Creates a basic SSL context. This is the SSL context used if you make an--- @\"https:\/\/\"@ request using one of the convenience functions. It--- configures OpenSSL to use the default set of ciphers.------ On Linux systems, this function also configures OpenSSL to verify--- certificates using the system certificates stored in @\/etc\/ssl\/certs@.------ On other systems, /no certificate validation is performed/ by the--- generated 'SSLContext' because there is no canonical place to find--- the set of system certificates. When using this library on a--- non-Linux system, you are encouraged to install the system--- certificates somewhere and create your own 'SSLContext'.----{--    We would like to turn certificate verification on for everyone, but-    this has proved contingent on leveraging platform specific mechanisms-    to reach the certificate store. That logic should probably be in-    hsopenssl, but feel free to change this as appropriate for your OS.--}-baselineContextSSL :: IO SSLContext-baselineContextSSL = do-    ctx <- SSL.context-    SSL.contextSetDefaultCiphers ctx-#if defined __MACOSX__-    SSL.contextSetVerificationMode ctx SSL.VerifyNone-#elif defined __WINDOWS__-    SSL.contextSetVerificationMode ctx SSL.VerifyNone-#else-    SSL.contextSetCADirectory ctx "/etc/ssl/certs"-    SSL.contextSetVerificationMode ctx $-        SSL.VerifyPeer True True Nothing-#endif-    return ctx---parseURL :: URL -> URI-parseURL r' =-    case parseURI r of-        Just u  -> u-        Nothing -> error ("Can't parse URI " ++ r)-  where-    r = T.unpack $ T.decodeUtf8 r'----------------------------------------------------------------------------------{--    Account for bug where "http://www.example.com" is parsed with no-    path element, resulting in an illegal HTTP request line.--}--path :: URI -> ByteString-path u = case url of-            ""  -> "/"-            _   -> url-  where-    url = T.encodeUtf8 $! T.pack-                      $! concat [uriPath u, uriQuery u, uriFragment u]---------------------------------------------------------------------------------------- | Issue an HTTP GET request and pass the resultant response to the--- supplied handler function. This code will silently follow redirects,--- to a maximum depth of 5 hops.------ The handler function is as for 'receiveResponse', so you can use one--- of the supplied convenience handlers if you're in a hurry:------ >     x' <- get "http://www.bbc.co.uk/news/" concatHandler------ But as ever the disadvantage of doing this is that you're not doing--- anything intelligent with the HTTP response status code. If you want--- an exception raised in the event of a non @2xx@ response, you can use:------ >     x' <- get "http://www.bbc.co.uk/news/" concatHandler'------ but for anything more refined you'll find it easy to simply write--- your own handler function.----get :: URL-    -- ^ Resource to GET from.-    -> (Response -> InputStream ByteString -> IO β)-    -- ^ Handler function to receive the response from the server.-    -> IO β-get r' handler = getN 0 r' handler--getN n r' handler = do-    bracket-        (establish u)-        (teardown)-        (process)--  where-    teardown = closeConnection--    u = parseURL r'--    process c = do-        q <- buildRequest $ do-            http GET (path u)-            setAccept "*/*"--        sendRequest c q emptyBody--        receiveResponse c (wrapRedirect n handler)---{--    This is fairly simple-minded. Improvements could include reusing-    the Connection if the redirect is to the same host, and closing-    the original Connection if it is not. These are both things that-    can be done manually if using the full API, so not worried about-    it for now.--}--wrapRedirect-    :: Int-    -> (Response -> InputStream ByteString -> IO β)-    -> Response-    -> InputStream ByteString-    -> IO β-wrapRedirect n handler p i = do-    if (s == 301 || s == 302 || s == 303 || s == 307)-        then case lm of-                Just l  -> getN n' l handler-                Nothing -> handler p i-        else handler p i-  where-    s  = getStatusCode p-    lm = getHeader p "Location"-    !n' = if n < 5-            then n + 1-            else throw $! TooManyRedirects n--data TooManyRedirects = TooManyRedirects Int-        deriving (Typeable, Show, Eq)--instance Exception TooManyRedirects-------- | Send content to a server via an HTTP POST request. Use this--- function if you have an 'OutputStream' with the body content.----post :: URL-    -- ^ Resource to POST to.-    -> ContentType-    -- ^ MIME type of the request body being sent.-    -> (OutputStream Builder -> IO α)-    -- ^ Handler function to write content to server.-    -> (Response -> InputStream ByteString -> IO β)-    -- ^ Handler function to receive the response from the server.-    -> IO β-post r' t body handler = do-    bracket-        (establish u)-        (teardown)-        (process)-  where-    teardown = closeConnection--    u = parseURL r'--    process c = do-        q <- buildRequest $ do-            http POST (path u)-            setAccept "*/*"-            setContentType t--        _ <- sendRequest c q body--        x <- receiveResponse c handler-        return x-------- | Send form data to a server via an HTTP POST request. This is the--- usual use case; most services expect the body to be MIME type--- @application/x-www-form-urlencoded@ as this is what conventional--- web browsers send on form submission. If you want to POST to a URL--- with an arbitrary Content-Type, use 'post'.----postForm-    :: URL-    -- ^ Resource to POST to.-    -> [(ByteString, ByteString)]-    -- ^ List of name=value pairs. Will be sent URL-encoded.-    -> (Response -> InputStream ByteString -> IO β)-    -- ^ Handler function to receive the response from the server.-    -> IO β-postForm r' nvs handler = do-    bracket-        (establish u)-        (teardown)-        (process)-  where-    teardown = closeConnection--    u = parseURL r'--    process c = do-        q <- buildRequest $ do-            http POST (path u)-            setAccept "*/*"-            setContentType "application/x-www-form-urlencoded"--        _ <- sendRequest c q (encodedFormBody nvs)--        x <- receiveResponse c handler-        return x-------- | Specify name/value pairs to be sent to the server in the manner--- used by web browsers when submitting a form via a POST request.--- Parameters will be URL encoded per RFC 2396 and combined into a--- single string which will be sent as the body of your request.------ You use this partially applied:------ >     let nvs = [("name","Kermit"),--- >                ("type","frog")]--- >                ("role","stagehand")]--- >--- >     sendRequest c q (encodedFormBody nvs)------ Note that it's going to be up to you to call 'setContentType' with--- a value of @\"application/x-www-form-urlencoded\"@ when building the--- Request object; the 'postForm' convenience (which uses this--- @encodedFormBody@ function) takes care of this for you, obviously.----encodedFormBody :: [(ByteString,ByteString)] -> OutputStream Builder -> IO ()-encodedFormBody nvs o = do-    Streams.write (Just b) o-  where-    b = mconcat $ intersperse (Builder.fromString "&") $ map combine nvs--    combine :: (ByteString,ByteString) -> Builder-    combine (n',v') = mconcat [urlEncodeBuilder n', Builder.fromString "=", urlEncodeBuilder v']-------- | Place content on the server at the given URL via an HTTP PUT--- request, specifying the content type and a function to write the--- content to the supplied 'OutputStream'. You might see:------ >     put "http://s3.example.com/bucket42/object149" "text/plain"--- >         (fileBody "hello.txt") (\p i -> do--- >             putStr $ show p--- >             Streams.connect i stdout)----put :: URL-    -- ^ Resource to PUT to.-    -> ContentType-    -- ^ MIME type of the request body being sent.-    -> (OutputStream Builder -> IO α)-    -- ^ Handler function to write content to server.-    -> (Response -> InputStream ByteString -> IO β)-    -- ^ Handler function to receive the response from the server.-    -> IO β-put r' t body handler = do-    bracket-        (establish u)-        (teardown)-        (process)-  where-    teardown = closeConnection--    u = parseURL r'--    process c = do-        q <- buildRequest $ do-            http PUT (path u)-            setAccept "*/*"-            setHeader "Content-Type" t--        _ <- sendRequest c q body--        x <- receiveResponse c handler-        return x-------- | A special case of 'concatHandler', this function will return the--- entire response body as a single ByteString, but will throw an--- exception if the response status code was other than @2xx@.----concatHandler' :: Response -> InputStream ByteString -> IO ByteString-concatHandler' p i =-    if s >= 300-        then throw (HttpClientError s m)-        else concatHandler p i-  where-    s = getStatusCode p-    m = getStatusMessage p--data HttpClientError = HttpClientError Int ByteString-        deriving (Typeable)--instance Exception HttpClientError--instance Show HttpClientError where-    show (HttpClientError s msg) = Prelude.show s ++ " " ++ S.unpack msg--{--    There should probably also be HttpServerError and maybe even-    HttpRedirectError, but as these names don't seem to show up-    in the runtime when raised, not sure it's worth the bother. It's-    not like we'd want anything different in their Show instances.--}------- | If you're working with a data stream that is in @application/json@,--- then chances are you're using @aeson@ to handle the JSON to Haskell--- decoding. If so, then this helper function might be of use.------ >     v <- get "http://api.example.com/v1/" jsonHandler------ This function feeds the input body to the 'Data.Aeson.Parser.json''--- @attoparsec@ Parser in order to get the aeson Value type. This is then--- marshalled to your type represeting the source data, via the FromJSON--- typeclass.------ The above example was actually insufficient; when working with--- @aeson@ you need to fix the type so it knows what FromJSON instance--- to use. Let's say you're getting Person objects, then it would be------ >     v <- get "http://api.example.com/v1/person/461" jsonHandler :: IO Person------ assuming your Person type had a FromJSON instance, of course.------ /Note/------ This function parses a single top level JSON object or array, which--- is all you're supposed to get if it's a valid document. People do--- all kinds of crazy things though, so beware. Also, this function (like the--- "concatHander" convenience) loads the entire response into memory; it's--- not /streaming/; if you're receiving a document which is (say) a very--- long array of objects then you may want to implement your own--- handler function, perhaps using "Streams.parserToInputStream" and--- the 'Data.Aeson.Parser' combinators directly — with a result type of--- InputStream Value, perhaps — by which you could then iterate over--- the Values one at a time in constant space.----{--    This looks simple. It wasn't. The types involved are rediculous to-    disentangle. The biggest problem is that the Parser type used in-    [aeson] is *NOT* the Parser type from [attoparsec]. But the parsing-    function `json` and `json` from Aeson use the attoparsec Parser even-    though the rest of the top level page is all about Aeson's parser as-    used in FromJSON!--    Anyway, `json` and `json'` are [attoparsec] Parser [aeson] Value; we-    run that using the [io-streams] convenience function-    `parseFromStream` which gets us a Value which is the intermediate-    abstract syntax tree for a  JSON document. Then (and this was hard-    to find) to work with that in terms of the FromJSON typeclass, you-    use the `fromJSON` function which has type (FromJSON α => Value ->-    Result α). Then finally, pull the result out of it. Why in Bog's-    name this wasn't just Either I'll never know.--}-jsonHandler-    :: (FromJSON α)-    => Response-    -> InputStream ByteString-    -> IO α-jsonHandler _ i = do-    v <- Streams.parseFromStream json' i        -- Value-    let r = fromJSON v                          -- Result-    case r of-        (Success a) ->  return a-        (Error str) ->  error str-
− src/Network/Http/RequestBuilder.hs
@@ -1,300 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.-----{-# LANGUAGE CPP                        #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings          #-}--module Network.Http.RequestBuilder (-    RequestBuilder,-    buildRequest,-    http,-    setHostname,-    setAccept,-    setAccept',-    setAuthorizationBasic,-    ContentType,-    setContentType,-    setContentLength,-    setExpectContinue,-    setHeader-) where--import Blaze.ByteString.Builder (Builder)-import qualified Blaze.ByteString.Builder as Builder (fromByteString,-                                                      toByteString)-import qualified Blaze.ByteString.Builder.Char8 as Builder (fromShow,-                                                            fromString)-import Control.Monad.State-import Data.ByteString (ByteString)-import qualified Data.ByteString.Base64 as BS64-import Data.ByteString.Char8 ()-import qualified Data.ByteString.Char8 as S-import Data.Int (Int64)-import Data.List (intersperse)-import Data.Monoid (mconcat)--import Network.Http.Connection-import Network.Http.Types--#include "config.h"------- | The RequestBuilder monad allows you to abuse do-notation to--- conveniently setup a 'Request' object.----newtype RequestBuilder α = RequestBuilder (State Request α)-  deriving (Monad, MonadState Request)------- | Run a RequestBuilder, yielding a Request object you can use on the--- given connection.------ >     q <- buildRequest $ do--- >         http POST "/api/v1/messages"--- >         setContentType "application/json"--- >         setHostname "clue.example.com" 80--- >         setAccept "text/html"--- >         setHeader "X-WhoDoneIt" "The Butler"------ Obviously it's up to you to later actually /send/ JSON data.------ /Note/------ The API of this function changed from version 0.3.1 to verison 0.4.0;--- the original requirement to pass a Connection object has been removed,--- thereby allowing you to build your Request before opening the--- connection to the web server.----buildRequest :: RequestBuilder α -> IO Request-buildRequest mm = do-    let (RequestBuilder s) = (mm)-    let q = Request {-        qHost = Nothing,-        qMethod = GET,-        qPath = "/",-        qBody = Empty,-        qExpect = Normal,-        qHeaders = emptyHeaders-    }-    return $ execState s q-------- | Begin constructing a Request, starting with the request line.----http :: Method -> ByteString -> RequestBuilder ()-http m p' = do-    q <- get-    let h0 = qHeaders q-    let h1 = updateHeader h0 "User-Agent" VERSION-    let h2 = updateHeader h1 "Accept-Encoding" "gzip"--    let e  = case m of-            GET   -> Empty-            TRACE -> Empty-            _     -> Chunking--    let h3 = case e of-            Chunking    -> updateHeader h2 "Transfer-Encoding" "chunked"-            _           -> h2--    put q {-        qMethod = m,-        qPath = p',-        qBody = e,-        qHeaders = h3-    }------- | Set the [virtual] hostname for the request. In ordinary conditions--- you won't need to call this, as the @Host:@ header is a required--- header in HTTP 1.1 and is set directly from the name of the server--- you connected to when calling 'Network.Http.Connection.openConnection'.----setHostname :: Hostname -> Port -> RequestBuilder ()-setHostname h' p = do-    q <- get-    put q {-        qHost = Just v'-    }-  where-    v' :: ByteString-    v' = if p == 80-        then h'-        else Builder.toByteString $ mconcat-           [Builder.fromByteString h',-            Builder.fromString ":",-            Builder.fromShow p]------- | Set a generic header to be sent in the HTTP request. The other--- methods in the RequestBuilder API are expressed in terms of this--- function, but we recommend you use them where offered for their--- stronger types.----setHeader :: ByteString -> ByteString -> RequestBuilder ()-setHeader k' v' = do-    q <- get-    let h0 = qHeaders q-    let h1 = updateHeader h0 k' v'-    put q {-        qHeaders = h1-    }--deleteHeader :: ByteString -> RequestBuilder ()-deleteHeader k' = do-    q <- get-    let h0 = qHeaders q-    let h1 = removeHeader h0 k'-    put q {-        qHeaders = h1-    }--{-# INLINE setEntityBody #-}-setEntityBody :: EntityBody -> RequestBuilder ()-setEntityBody e = do-    q <- get-    put q {-        qBody = e-    }--{-# INLINE setExpectMode #-}-setExpectMode :: ExpectMode -> RequestBuilder ()-setExpectMode e = do-    q <- get-    put q {-        qExpect = e-    }------- | Indicate the content type you are willing to receive in a reply--- from the server. For more complex @Accept:@ headers, use--- 'setAccept''.----setAccept :: ByteString -> RequestBuilder ()-setAccept v' = do-    setHeader "Accept" v'------- | Indicate the content types you are willing to receive in a reply--- from the server in order of preference. A call of the form:------ >         setAccept' [("text/html", 1.0),--- >                     ("application/xml", 0.8),--- >                     ("*/*", 0)]------ will result in an @Accept:@ header value of--- @text\/html; q=1.0, application\/xml; q=0.8, \*\/\*; q=0.0@ as you--- would expect.----setAccept' :: [(ByteString,Float)] -> RequestBuilder ()-setAccept' tqs = do-    setHeader "Accept" v'-  where-    v' = Builder.toByteString v-    v  = mconcat $ intersperse (Builder.fromString ", ") $ map format tqs--    format :: (ByteString,Float) -> Builder-    format (t',q) =-        mconcat-           [Builder.fromByteString t',-            Builder.fromString "; q=",-            Builder.fromShow q]-------- | Set username and password credentials per the HTTP basic--- authentication method.------ >         setAuthorizationBasic "Aladdin" "open sesame"------ will result in an @Authorization:@ header value of--- @Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==@.------ Basic authentication does /not/ use a message digest function to--- encipher the password; the above string is only base-64 encoded and--- is thus plain-text visible to any observer on the wire and all--- caches and servers at the other end, making basic authentication--- completely insecure. A number of web services, however, use SSL to--- encrypt the connection that then use HTTP basic authentication to--- validate requests. Keep in mind in these cases the secret is still--- sent to the servers on the other side and passes in clear through--- all layers after the SSL termination. Do /not/ use basic--- authentication to protect secure or user-originated privacy-sensitve--- information.----{--    This would be better using Builder, right?--}-setAuthorizationBasic :: ByteString -> ByteString -> RequestBuilder ()-setAuthorizationBasic user' passwd' = do-    setHeader "Authorization" v'-  where-    v'   = S.concat ["Basic ", msg']-    msg' = BS64.encode str'-    str' = S.concat [user', ":", passwd']---type ContentType = ByteString-------- | Set the MIME type corresponding to the body of the request you are--- sending. Defaults to @\"text\/plain\"@, so usually you need to set--- this if 'PUT'ting.----setContentType :: ContentType -> RequestBuilder ()-setContentType v' = do-    setHeader "Content-Type" v'------- | Specify the length of the request body, in bytes.------ RFC 2616 requires that we either send a @Content-Length@ header or--- use @Transfer-Encoding: chunked@. If you know the exact size ahead--- of time, then call this function; the body content will still be--- streamed out by @io-streams@ in more-or-less constant space.------ This function is special: in a PUT or POST request, @http-streams@--- will assume chunked transfer-encoding /unless/ you specify a content--- length here, in which case you need to ensure your body function--- writes precisely that many bytes.-------setContentLength :: Int64 -> RequestBuilder ()-setContentLength n = do-    deleteHeader "Transfer-Encoding"-    setHeader "Content-Length" (S.pack $ show n)-    setEntityBody $ Static n------- | Specify that this request should set the expectation that the--- server needs to approve the request before you send it.------ This function is special: in a PUT or POST request, @http-streams@--- will wait for the server to reply with an HTTP/1.1 100 Continue--- status before sending the entity body. This is handled internally;--- you will get the real response (be it successful 2xx, client error,--- 4xx, or server error 5xx) in 'receiveResponse'. In theory, it--- should be 417 if the expectation failed.------ Only bother with this if you know the service you're talking to--- requires clients to send an @Expect: 100-continue@ header and will--- handle it properly. Most servers don't do any precondition checking,--- automatically send an intermediate 100 response, and then just read--- the body regardless, making this a bit of a no-op in most cases.----setExpectContinue :: RequestBuilder ()-setExpectContinue = do-    setHeader "Expect" "100-continue"-    setExpectMode Continue-
− src/Network/Http/ResponseParser.hs
@@ -1,275 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.------ Significant portions of this file were written while studying--- the HTTP request parser implementation in the Snap Framework;--- snap-core's src/Snap/Internal/Parsing.hs and snap-server's--- src/Snap/Internal/Http/Parser.hs, and various utility functions--- have been cloned from there.-----{-# LANGUAGE BangPatterns       #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE OverloadedStrings  #-}--module Network.Http.ResponseParser (-    readResponseHeader,-    readResponseBody,--        -- for testing-    readDecimal-) where--import Prelude hiding (take, takeWhile)--import Control.Applicative-import Control.Exception (Exception, throwIO)-import Control.Monad (void)-import Control.Monad.IO.Class (liftIO)-import Data.Attoparsec.ByteString.Char8-import Data.Bits (Bits (..))-import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as S-import Data.CaseInsensitive (mk)-import Data.Char (ord)-import Data.Int (Int64)-import Data.Typeable (Typeable)-import System.IO.Streams (Generator, InputStream)-import qualified System.IO.Streams as Streams-import qualified System.IO.Streams.Attoparsec as Streams--import Network.Http.Types-import Network.Http.Utilities--{--    The chunk size coming down from the server is somewhat arbitrary;-    it's really just an indication of how many bytes need to be read-    before the next size marker or end marker - neither of which has-    anything to do with streaming on our side. Instead, we'll feed-    bytes into our InputStream at an appropriate intermediate size.--}-__BITE_SIZE__ :: Int-__BITE_SIZE__ = 32 * 1024---{--    Process the reply from the server up to the end of the headers as-    deliniated by a blank line.--}-readResponseHeader :: InputStream ByteString -> IO Response-readResponseHeader i = do-    (sc,sm) <- Streams.parseFromStream parseStatusLine i--    hs <- readHeaderFields i--    let h  = buildHeaders hs-    let te = case lookupHeader h "Transfer-Encoding" of-            Just x' -> if mk x' == "chunked"-                        then Chunked-                        else None-            Nothing -> None--    let ce = case lookupHeader h "Content-Encoding" of-            Just x' -> if mk x' == "gzip"-                        then Gzip-                        else Identity-            Nothing -> Identity--    let nm = case lookupHeader h "Content-Length" of-            Just x' -> Just (readDecimal x' :: Int64)-            Nothing -> case sc of-                        204 -> Just 0-                        304 -> Just 0-                        100 -> Just 0-                        _   -> Nothing--    return Response {-        pStatusCode = sc,-        pStatusMsg = sm,-        pTransferEncoding = te,-        pContentEncoding = ce,-        pContentLength = nm,-        pHeaders = h-    }---parseStatusLine :: Parser (Int,ByteString)-parseStatusLine = do-    sc <- string "HTTP/1." *> satisfy version *> char ' ' *> decimal <* char ' '-    sm <- takeTill (== '\r') <* crlf-    return (sc,sm)-  where-    version c = c == '1' || c == '0'---crlf :: Parser ByteString-crlf = string "\r\n"--------------------------------------------------------------------------{--    Switch on the encoding and compression headers, wrapping the raw-    InputStream to present the entity body's actual bytes.--}-readResponseBody :: Response -> InputStream ByteString -> IO (InputStream ByteString)-readResponseBody p i1 = do--    i2 <- case t of-        None        -> case l of-                        Just n  -> readFixedLengthBody i1 n-                        Nothing -> readUnlimitedBody i1-        Chunked     -> readChunkedBody i1--    i3 <- case c of-        Identity    -> return i2-        Gzip        -> readCompressedBody i2-        Deflate     -> throwIO (UnexpectedCompression $ show c)--    return i3-  where-    t = pTransferEncoding p-    c = pContentEncoding p-    l = pContentLength p---readDecimal :: (Enum α, Num α, Bits α) => ByteString -> α-readDecimal str' =-    S.foldl' f 0 x'-  where-    f !cnt !i = cnt * 10 + digitToInt i--    x' = head $ S.words str'--    {-# INLINE digitToInt #-}-    digitToInt :: (Enum α, Num α, Bits α) => Char -> α-    digitToInt c | c >= '0' && c <= '9' = toEnum $! ord c - ord '0'-                 | otherwise = error $ "'" ++ [c] ++ "' is not an ascii digit"-{-# INLINE readDecimal #-}--data UnexpectedCompression = UnexpectedCompression String-        deriving (Typeable, Show)--instance Exception UnexpectedCompression--------------------------------------------------------------------------{--    Process a response body in chunked transfer encoding, taking the-    resultant bytes and reproducing them as an InputStream--}-readChunkedBody :: InputStream ByteString -> IO (InputStream ByteString)-readChunkedBody i1 = do-    i2 <- Streams.fromGenerator (consumeChunks i1)-    return i2---{--    For a response body in chunked transfer encoding, iterate over-    the individual chunks, reading the size parameter, then-    looping over that chunk in bites of at most __BYTE_SIZE__,-    yielding them to the receiveResponse InputStream accordingly.--}-consumeChunks :: InputStream ByteString -> Generator ByteString ()-consumeChunks i1 = do-    !n <- parseSize--    if n > 0-        then do-            -- read one or more bites, then loop to next chunk-            go n-            skipCRLF-            consumeChunks i1-        else do-            -- skip "trailers" and consume final CRLF-            skipEnd--  where-    go 0 = return ()-    go !n = do-        (!x',!r) <- liftIO $ readN n i1-        Streams.yield x'-        go r--    parseSize = do-        n <- liftIO $ Streams.parseFromStream transferChunkSize i1-        return n--    skipEnd = do-        liftIO $ do-            _ <- readHeaderFields i1-            return ()--    skipCRLF = do-        liftIO $ do-            _ <- Streams.parseFromStream crlf i1-            return ()--{--    Read the specified number of bytes up to a maximum of __BITE_SIZE__,-    returning a resultant ByteString and the number of bytes remaining.--}--readN :: Int -> InputStream ByteString -> IO (ByteString, Int)-readN n i1 = do-    !x' <- Streams.readExactly p i1-    return (x', r)-  where-    !d = n - size--    !p = if d > 0-        then size-        else n--    !r = if d > 0-        then d-        else 0--    size = __BITE_SIZE__---transferChunkSize :: Parser (Int)-transferChunkSize = do-    !n <- hexadecimal-    void (takeTill (== '\r'))-    void crlf-    return n--------------------------------------------------------------------------{--    This has the rather crucial side effect of terminating the stream-    after the requested number of bytes. Otherwise, code handling-    responses waits on more input until an HTTP timeout occurs.--}-readFixedLengthBody :: InputStream ByteString -> Int64 -> IO (InputStream ByteString)-readFixedLengthBody i1 n = do-    i2 <- Streams.takeBytes n i1-    return i2--{--    On the other hand, there is the (predominently HTTP/1.0) case-    where there is no content length sent and no chunking, with the-    result that only the connection closing marks the end of the-    response body.--}-readUnlimitedBody :: InputStream ByteString -> IO (InputStream ByteString)-readUnlimitedBody i1 = do-    return i1--------------------------------------------------------------------------readCompressedBody :: InputStream ByteString -> IO (InputStream ByteString)-readCompressedBody i1 = do-    i2 <- Streams.gunzip i1-    return i2
− src/Network/Http/Types.hs
@@ -1,387 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.-----{-# LANGUAGE BangPatterns       #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE OverloadedStrings  #-}--module Network.Http.Types (-    Request(..),-    EntityBody(..),-    ExpectMode(..),-    Response(..),-    StatusCode,-    TransferEncoding(..),-    ContentEncoding(..),-    getStatusCode,-    getStatusMessage,-    getHeader,-    Method(..),-    Headers,-    emptyHeaders,-    updateHeader,-    removeHeader,-    buildHeaders,-    lookupHeader,-    HttpParseException(..),--    -- for testing-    composeRequestBytes,-    composeResponseBytes-) where--import Prelude hiding (lookup)--import Blaze.ByteString.Builder (Builder)-import qualified Blaze.ByteString.Builder as Builder (copyByteString,-                                                      copyByteString,-                                                      fromByteString,-                                                      fromByteString,-                                                      toByteString)-import qualified Blaze.ByteString.Builder.Char8 as Builder (fromChar,-                                                            fromShow,-                                                            fromString)-import Control.Exception (Exception)-import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as S-import Data.CaseInsensitive (CI, mk, original)-import Data.HashMap.Strict (HashMap, delete, empty, foldrWithKey, insert,-                            insertWith, lookup)-import Data.Int (Int64)-import Data.List (foldl')-import Data.Monoid (mconcat, mempty)-import Data.Typeable (Typeable)---- | HTTP Methods, as per RFC 2616-data Method-    = GET-    | HEAD-    | POST-    | PUT-    | DELETE-    | TRACE-    | OPTIONS-    | CONNECT-    | PATCH-    | Method ByteString-        deriving (Show, Read, Ord)---instance Eq Method where-    GET          == GET              = True-    HEAD         == HEAD             = True-    POST         == POST             = True-    PUT          == PUT              = True-    DELETE       == DELETE           = True-    TRACE        == TRACE            = True-    OPTIONS      == OPTIONS          = True-    CONNECT      == CONNECT          = True-    PATCH        == PATCH            = True-    GET          == Method "GET"     = True-    HEAD         == Method "HEAD"    = True-    POST         == Method "POST"    = True-    PUT          == Method "PUT"     = True-    DELETE       == Method "DELETE"  = True-    TRACE        == Method "TRACE"   = True-    OPTIONS      == Method "OPTIONS" = True-    CONNECT      == Method "CONNECT" = True-    PATCH        == Method "PATCH"   = True-    Method a     == Method b         = a == b-    m@(Method _) == other            = other == m-    _            == _                = False------- | A description of the request that will be sent to the server. Note--- unlike other HTTP libraries, the request body is /not/ a part of this--- object; that will be streamed out by you when actually sending the--- request with 'sendRequest'.------ 'Request' has a useful @Show@ instance that will output the request--- line and headers (as it will be sent over the wire but with the @\\r@--- characters stripped) which can be handy for debugging.------ Note that the @Host:@ header is not set until--- 'Network.Http.Connection.sendRequest' is called, so you will not see--- it in the Show instance (unless you call 'setHostname' to override--- the value inherited from the @Connection@).----data Request-    = Request {-        qMethod  :: !Method,-        qHost    :: !(Maybe ByteString),-        qPath    :: !ByteString,-        qBody    :: !EntityBody,-        qExpect  :: !ExpectMode,-        qHeaders :: !Headers-    }--instance Show Request where-    show q = {-# SCC "Request.show" #-}-        S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ composeRequestBytes q "<default>"---data EntityBody = Empty | Chunking | Static Int64--data ExpectMode = Normal | Continue--{--    The bit that builds up the actual string to be transmitted. This-    is on the critical path for every request, so we'll want to revisit-    this to improve performance.--    - Rewrite rule for Method?-    - How can serializing the Headers be made efficient?--    This code includes the RFC compliant CR-LF sequences as line-    terminators, which is why the Show instance above has to bother-    with removing them.--}--composeRequestBytes :: Request -> ByteString -> Builder-composeRequestBytes q h' =-    mconcat-       [requestline,-        hostLine,-        headerFields,-        crlf]-  where-    requestline = mconcat-       [method,-        sp,-        uri,-        sp,-        version,-        crlf]--    method = case qMethod q of-        GET     -> Builder.fromString "GET"-        HEAD    -> Builder.fromString "HEAD"-        POST    -> Builder.fromString "POST"-        PUT     -> Builder.fromString "PUT"-        DELETE  -> Builder.fromString "DELETE"-        TRACE   -> Builder.fromString "TRACE"-        OPTIONS -> Builder.fromString "OPTIONS"-        CONNECT -> Builder.fromString "CONNECT"-        PATCH   -> Builder.fromString "PATCH"-        (Method x) -> Builder.fromByteString x--    uri = Builder.copyByteString $ qPath q--    version = Builder.fromString "HTTP/1.1"--    hostLine = mconcat-       [Builder.fromString"Host: ",-        hostname,-        crlf]--    hostname = case qHost q of-        Just x' -> Builder.copyByteString x'-        Nothing -> Builder.copyByteString h'--    headerFields = joinHeaders $ unWrap $ qHeaders q---crlf = Builder.fromString "\r\n"--sp = Builder.fromChar ' '---type StatusCode = Int------- | A description of the response received from the server. Note--- unlike other HTTP libraries, the response body is /not/ a part--- of this object; that will be streamed in by you when calling--- 'receiveResponse'.------ Like 'Request', 'Response' has a @Show@ instance that will output--- the status line and response headers as they were received from the--- server.----data Response-    = Response {-        pStatusCode       :: !StatusCode,-        pStatusMsg        :: !ByteString,-        pTransferEncoding :: !TransferEncoding,-        pContentEncoding  :: !ContentEncoding,-        pContentLength    :: !(Maybe Int64),-        pHeaders          :: !Headers-    }--instance Show Response where-    show p =     {-# SCC "Response.show" #-}-        S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ composeResponseBytes p---data TransferEncoding = None | Chunked--data ContentEncoding = Identity | Gzip | Deflate-    deriving (Show)-------- | Get the HTTP response status code.----getStatusCode :: Response -> StatusCode-getStatusCode = pStatusCode-{-# INLINE getStatusCode #-}------- | Get the HTTP response status message. Keep in mind that this is--- /not/ normative; whereas 'getStatusCode' values are authoritative.----getStatusMessage :: Response -> ByteString-getStatusMessage = pStatusMsg-{-# INLINE getStatusMessage #-}------- | Lookup a header in the response. HTTP header field names are--- case-insensitive, so you can specify the name to lookup however you--- like. If the header is not present @Nothing@ will be returned.------ >     let n = case getHeader p "Content-Length" of--- >                Just x' -> read x' :: Int--- >                Nothing -> 0------ which of course is essentially what goes on inside the library when--- @http-streams@ receives a response from the server and has to figure--- out how many bytes to read.------ There is a fair bit of complexity in some of the other HTTP response--- fields, so there are a number of specialized functions for reading--- those values where we've found them useful.----getHeader :: Response -> ByteString -> Maybe ByteString-getHeader p k =-    lookupHeader h k-  where-    h = pHeaders p---composeResponseBytes :: Response -> Builder-composeResponseBytes p =-    mconcat-       [statusline,-        headerFields,-        crlf]-  where-    statusline = mconcat-       [version,-        sp,-        code,-        sp,-        message,-        crlf]--    code = Builder.fromShow $ pStatusCode p--    message = Builder.copyByteString $ pStatusMsg p--    version = Builder.fromString "HTTP/1.1"--    headerFields = joinHeaders $ unWrap $ pHeaders p-------- | The map of headers in a 'Request' or 'Response'. Note that HTTP--- header field names are case insensitive, so if you call 'setHeader'--- on a field that's already defined but with a different capitalization--- you will replace the existing value.----{--    This is a fair bit of trouble just to avoid using a typedef here.-    Probably worth it, though; every other HTTP client library out there-    exposes the gory details of the underlying map implementation, and-    to use it you need to figure out all kinds of crazy imports. Indeed,-    this code used here in the Show instance for debugging has been-    copied & pasted around various projects of mine since I started-    writing Haskell. It's quite tedious, and very arcane! So, wrap it-    up.--}-newtype Headers = Wrap {-    unWrap :: HashMap (CI ByteString) ByteString-}--instance Show Headers where-    show x = S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ joinHeaders $ unWrap x--joinHeaders :: HashMap (CI ByteString) ByteString -> Builder-joinHeaders m = foldrWithKey combine mempty m--combine :: CI ByteString -> ByteString -> Builder -> Builder-combine k v acc =-    mconcat [acc, key, Builder.fromString ": ", value, crlf]-  where-    key = Builder.copyByteString $ original k-    value = Builder.fromByteString v-{-# INLINE combine #-}--emptyHeaders :: Headers-emptyHeaders =-    Wrap empty--{--    Set a header field to the specified value. This will overwrite-    any existing value for the field. Remember that HTTP fields names-    are case insensitive!--}-updateHeader :: Headers -> ByteString -> ByteString -> Headers-updateHeader x k v =-    Wrap result-  where-    !result = insert (mk k) v m-    !m = unWrap x--removeHeader :: Headers -> ByteString -> Headers-removeHeader x k =-    Wrap result-  where-    !result = delete (mk k) m-    !m = unWrap x---{--    Given a list of key,value pairs, construct a 'Headers' map. This is-    only going to be used by RequestBuilder and ResponseParser,-    obviously. And yes, as usual, we go to a lot of trouble to splice-    out the function doing the work, in the name of type sanity.--}-buildHeaders :: [(ByteString,ByteString)] -> Headers-buildHeaders hs =-    Wrap result-  where-    result = foldl' addHeader empty hs--{--    insertWith is used here for the case where a header is repeated-    (for example, Set-Cookie) and the values need to be intercalated-    with ',' as per RFC 2616 §4.2.--}-addHeader-    :: HashMap (CI ByteString) ByteString-    -> (ByteString,ByteString)-    -> HashMap (CI ByteString) ByteString-addHeader m (k,v) =-    insertWith f (mk k) v m-  where-    f new old = S.concat [old, ",", new]--lookupHeader :: Headers -> ByteString -> Maybe ByteString-lookupHeader x k =-    lookup (mk k) m-  where-    !m = unWrap x---data HttpParseException = HttpParseException String-        deriving (Typeable, Show)--instance Exception HttpParseException
− src/Network/Http/Utilities.hs
@@ -1,272 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is--- made available to you by its authors as open source software:--- you can redistribute it and/or modify it under the terms of--- the BSD licence.------ This file is essentially a clone of Snap.Internal.Parsing,--- the HTTP request parser implementation in the Snap Framework;--- snap-core's src/Snap/Internal/Parsing.hs and snap-server's--- src/Snap/Internal/Http/Parser.hs, copied here to specialize--- it to Response parsing. This code replaces the attoparsec--- based implementation formerly in ResponseParser, but is--- kept separate to aid syncing changes from snap-core as they--- become available.-----{-# LANGUAGE BangPatterns       #-}-{-# LANGUAGE CPP                #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE MagicHash          #-}-{-# LANGUAGE OverloadedStrings  #-}-{-# LANGUAGE Rank2Types         #-}-{-# LANGUAGE Trustworthy        #-}-{-# LANGUAGE UnboxedTuples      #-}--module Network.Http.Utilities (-    readResponseLine,-    readHeaderFields-) where---------------------------------------------------------------------------------import Control.Exception (throwIO)-import Control.Monad (when)-import Data.Bits-import qualified Data.ByteString.Char8 as S-import Data.ByteString.Internal (ByteString, w2c)-import qualified Data.ByteString.Unsafe as S-import Data.Char hiding (digitToInt, isDigit, isSpace)-import GHC.Exts (Int (..), Int#, (+#))-import Prelude hiding (head, take, takeWhile)-import System.IO.Streams (InputStream)-import qualified System.IO.Streams as Streams-------------------------------------------------------------------------------import Network.Http.Types----------------------------------------------------------------------------------{--    This is vestigial; originally it was the Request parsing-    code in Snap. Keeping it here until we can use if for-    response parsing.--}-parseRequest :: InputStream ByteString -> IO (Maybe Request)-parseRequest input = do-    eof <- Streams.atEOF input-    if eof-      then return Nothing-      else do-        line <- readResponseLine input-        let (!mStr,!s)      = bSp line-        let (!uri, !vStr)   = bSp s-        let !version        = pVer vStr :: (Int,Int)----      hdrs    <- readHeaderFields input-        return $! Nothing--  where--    pVer s = if "HTTP/" `S.isPrefixOf` s-               then pVers (S.unsafeDrop 5 s)-               else (1, 0)--    bSp   = splitCh ' '--    pVers s = (c, d)-      where-        (!a, !b)   = splitCh '.' s-        !c         = unsafeFromNat a-        !d         = unsafeFromNat b---{--    Read a single line of an HTTP response.--}-readResponseLine :: InputStream ByteString -> IO ByteString-readResponseLine input = go []-  where-    throwNoCRLF =-        throwIO $-        HttpParseException "parse error: expected line ending in crlf"--    throwBadCRLF =-        throwIO $-        HttpParseException "parse error: got cr without subsequent lf"--    go !l = do-        !mb <- Streams.read input-        !s  <- maybe throwNoCRLF return mb--        case findCRLF s of-            FoundCRLF idx# -> foundCRLF l s idx#-            NoCR           -> noCRLF l s-            LastIsCR idx#  -> lastIsCR l s idx#-            _              -> throwBadCRLF--    foundCRLF l s idx# = do-        let !i1 = (I# idx#)-        let !i2 = (I# (idx# +# 2#))-        let !a = S.unsafeTake i1 s-        when (i2 < S.length s) $ do-            let !b = S.unsafeDrop i2 s-            Streams.unRead b input--        -- Optimize for the common case: dl is almost always "id"-        let !out = if null l then a else S.concat (reverse (a:l))-        return out--    noCRLF l s = go (s:l)--    lastIsCR l s idx# = do-        !t <- Streams.read input >>= maybe throwNoCRLF return-        if S.null t-          then lastIsCR l s idx#-          else do-            let !c = S.unsafeHead t-            if c /= 10-              then throwBadCRLF-              else do-                  let !a = S.unsafeTake (I# idx#) s-                  let !b = S.unsafeDrop 1 t-                  when (not $ S.null b) $ Streams.unRead b input-                  let !out = if null l then a else S.concat (reverse (a:l))-                  return out----------------------------------------------------------------------------------data CS = FoundCRLF !Int#-        | NoCR-        | LastIsCR !Int#-        | BadCR----------------------------------------------------------------------------------findCRLF :: ByteString -> CS-findCRLF b =-    case S.elemIndex '\r' b of-      Nothing         -> NoCR-      Just !i@(I# i#) ->-          let !i' = i + 1-          in if i' < S.length b-               then if S.unsafeIndex b i' == 10-                      then FoundCRLF i#-                      else BadCR-               else LastIsCR i#-{-# INLINE findCRLF #-}----------------------------------------------------------------------------------splitCh :: Char -> ByteString -> (ByteString, ByteString)-splitCh !c !s = maybe (s, S.empty) f (S.elemIndex c s)-  where-    f !i = let !a = S.unsafeTake i s-               !b = S.unsafeDrop (i + 1) s-           in (a, b)-{-# INLINE splitCh #-}----------------------------------------------------------------------------------breakCh :: Char -> ByteString -> (ByteString, ByteString)-breakCh !c !s = maybe (s, S.empty) f (S.elemIndex c s)-  where-    f !i = let !a = S.unsafeTake i s-               !b = S.unsafeDrop i s-           in (a, b)-{-# INLINE breakCh #-}----------------------------------------------------------------------------------splitHeader :: ByteString -> (ByteString, ByteString)-splitHeader !s = maybe (s, S.empty) f (S.elemIndex ':' s)-  where-    l = S.length s--    f i = let !a = S.unsafeTake i s-          in (a, skipSp (i + 1))--    skipSp !i | i >= l    = S.empty-              | otherwise = let c = S.unsafeIndex s i-                            in if isLWS $ w2c c-                                 then skipSp $ i + 1-                                 else S.unsafeDrop i s--{-# INLINE splitHeader #-}-----------------------------------------------------------------------------------isLWS :: Char -> Bool-isLWS c = c == ' ' || c == '\t'-{-# INLINE isLWS #-}-----------------------------------------------------------------------------------{--    Read the remainder of the response message's header section,-    parsing into key/value pairs. Note that this function terminates-    when it hits the "blank" line (ie, CRLF CRLF pair), which it-    consumes.--}-readHeaderFields :: InputStream ByteString -> IO [(ByteString,ByteString)]-readHeaderFields input = do-    f <- go id-    return $! f []--  where-    go !dlistSoFar = do-        line <- readResponseLine input-        if S.null line-          then return dlistSoFar-          else do-            let (!k,!v) = splitHeader line-            vf <- pCont id-            let vs = vf []-            let !v' = if null vs then v else S.concat (v:vs)-            let !t = (k,v')-            go (dlistSoFar . (t:))--      where-        trimBegin = S.dropWhile isLWS--        pCont !dlist = do-            mbS  <- Streams.peek input-            maybe (return dlist)-                  (\s -> if S.null s-                           then Streams.read input >> pCont dlist-                           else if isLWS $ w2c $ S.unsafeHead s-                                  then procCont dlist-                                  else return dlist)-                  mbS--        procCont !dlist = do-            line <- readResponseLine input-            let !t = trimBegin line-            pCont (dlist . (" ":) . (t:))----                            ------------------------                            -- utility functions ---                            ----------------------------------------------------------------------------------------------------------- | Note: only works for nonnegative naturals-unsafeFromNat :: (Enum a, Num a, Bits a) => ByteString -> a-unsafeFromNat = S.foldl' f 0-  where-    zero = ord '0'-    f !cnt !i = cnt * 10 + toEnum (digitToInt i)--    digitToInt c = if d >= 0 && d <= 9-                     then d-                     else error $ "bad digit: '" ++ [c] ++ "'"-      where-        !d = ord c - zero-{-# INLINE unsafeFromNat #-}
− tests/Check.hs
@@ -1,25 +0,0 @@------ HTTP client for use with io-streams------ Copyright © 2012-2013 Operational Dynamics Consulting, Pty Ltd------ The code in this file, and the program it is a part of, is made--- available to you by its authors as open source software: you can--- redistribute it and/or modify it under a BSD licence.-----{-# LANGUAGE OverloadedStrings #-}-{-# OPTIONS -fno-warn-unused-imports #-}--module Main where--import OpenSSL (withOpenSSL)-import Test.Hspec (hspec)--import MockServer (runMockServer)-import TestSuite (suite)--main :: IO ()-main = withOpenSSL $ do-    runMockServer-    hspec suite
+ tests/MockServer.hs view
@@ -0,0 +1,306 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2021 Athae Eredh Siniath and Others+--+-- The code in this file, and the program it is a part of, is made+-- available to you by its authors as open source software: you can+-- redistribute it and/or modify it under a BSD licence.+--+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}++{-# OPTIONS -fno-warn-dodgy-imports #-}++module MockServer (runMockServer, localPort) where++{-+    Per http://hackage.haskell.org/trac/ghc/ticket/7167, we suppress+    the warning resulting from this line, necessary on <7.6+-}+import Prelude hiding (catch)++import Control.Applicative+import Control.Concurrent (forkIO, threadDelay)+import Control.Exception (SomeException)+import Control.Exception.Lifted (catch)+import "mtl" Control.Monad.Trans (liftIO)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as S+import qualified Data.ByteString.Lazy.Char8 as L+import Data.Maybe (fromMaybe)+import System.Directory (getFileSize)+import Snap.Core+import Snap.Http.Server+import Snap.Util.FileServe+import System.IO (hFlush, hPutStrLn, stderr)++import Network.Http.Client (Hostname, Port)++localHost = "localhost" :: Hostname+localPort = 56981 :: Port++main :: IO ()+main = go++{-+    Binding the port to the IPv4 localhost appears to settle the problem+    of localhost resolving ambigiously. If that doesn't work, we can+    comment out the setBind and the resultant 0.0.0.0 does seem to work.+-}+go :: IO ()+go = httpServe c site+  where+    c =+        setAccessLog ConfigNoLog $+            setErrorLog ConfigNoLog $+                setHostname localHost $+                    setBind localHost $+                        setPort (fromIntegral localPort) $+                            setVerbose False emptyConfig++runMockServer :: IO ()+runMockServer = do+    _ <- forkIO go+    threadDelay 2000000+    return ()++--+-- Top level URL routing logic.+--++site :: Snap ()+site =+    catch+        (routeRequests)+        (\e -> serveError "Splat\n" e)++routeRequests :: Snap ()+routeRequests =+    route+        [ ("resource/:id", serveResource)+        , ("static/:id", method GET serveStatic)+        , ("time", serveTime)+        , ("", ifTop handleAsText)+        , ("bounce", serveRedirect)+        , ("local", serveLocalRedirect)+        , ("loop", serveRedirectEndlessly)+        , ("empty", serveWithoutContent)+        , ("postbox", method POST handlePostMethod)+        , ("size", handleSizeRequest)+        , ("api", handleRestfulRequest)+        , ("cookies", serveRepeatedResponseHeaders)+        ]+        <|> serveNotFound++serveResource :: Snap ()+serveResource = do+    r <- getRequest++    let m = rqMethod r+    case m of+        GET -> handleGetMethod+        PUT -> handlePutWithExpectation+        _ -> serveMethodNotAllowed++serveStatic :: Snap ()+serveStatic = do+    im' <- getParam "id"++    let i' = fromMaybe "" im'+    let f' = S.concat ["tests/", i']+    let f = S.unpack f'++    l <- liftIO $ getFileSize f++    let t = fileType defaultMimeTypes f+    modifyResponse $ setContentType t+    modifyResponse $ setContentLength $ fromIntegral l+    b' <- liftIO $ S.readFile f+    writeBS b'++serveTime :: Snap ()+serveTime = do+    writeBS "Sun 30 Dec 12, 05:39:56.746Z\n"++--+-- Dispatch normal GET requests based on MIME type.+--++handleGetMethod :: Snap ()+handleGetMethod = do+    r <- getRequest+    let mime0 = getHeader "Accept" r++    case mime0 of+        Just "text/html" -> handleAsBrowser+        _ -> handleAsText++handleAsBrowser :: Snap ()+handleAsBrowser = do+    modifyResponse $ setResponseStatus 200 "OK"+    modifyResponse $ setContentType "text/html; charset=UTF-8"+    modifyResponse $ setHeader "Cache-Control" "max-age=1"+    sendFile "tests/hello.html"++handleAsText :: Snap ()+handleAsText = do+    modifyResponse $ setContentType "text/plain"+    writeBS "Sounds good to me\n"++handleRestfulRequest :: Snap ()+handleRestfulRequest = do+    modifyResponse $ setResponseStatus 200 "OK"+    modifyResponse $ setContentType "application/json"++    sendFile "tests/data-eu-gdp.json"++serveRedirect :: Snap ()+serveRedirect = do+    modifyResponse $ setResponseStatus 307 "Temporary Redirect"+    modifyResponse $ setHeader "Cache-Control" "no-cache"+    modifyResponse $ setHeader "Location" r'+  where+    r' = S.concat ["http://", localHost, ":", S.pack $ show $ localPort, "/time"]++serveLocalRedirect :: Snap ()+serveLocalRedirect = do+    modifyResponse $ setResponseStatus 307 "Temporary Redirect"+    modifyResponse $ setHeader "Cache-Control" "no-cache"+    modifyResponse $ setHeader "Location" r'+  where+    r' = S.pack "/time"++serveRedirectEndlessly :: Snap ()+serveRedirectEndlessly = do+    modifyResponse $ setResponseStatus 307 "Temporary Redirect"+    modifyResponse $ setHeader "Cache-Control" "no-cache"+    modifyResponse $ setHeader "Location" r'+  where+    r' = S.concat ["http://", localHost, ":", S.pack $ show $ localPort, "/loop"]++{-+    Attempt to test the bug with 204 No Content not closing in absence of a+    Content-Length header, however Snap automatically adds one, it seems. So,+    after the fact, this is unused and the case is tested in+    TestServer.testDevoidOfContent.+-}++serveWithoutContent :: Snap ()+serveWithoutContent = do+    modifyResponse $ setResponseStatus 204 "No Content"+    modifyResponse $ setHeader "Cache-Control" "no-cache"++serveRepeatedResponseHeaders :: Snap ()+serveRepeatedResponseHeaders = do+    modifyResponse $ addHeader "Set-Cookie" "stone=diamond"+    modifyResponse $ addHeader "Set-Cookie" "metal=tungsten"++handlePostMethod :: Snap ()+handlePostMethod = do+    setTimeout 5+    modifyResponse $ setResponseStatus 201 "Created"+    modifyResponse $ setHeader "Cache-Control" "no-cache"+    modifyResponse $ setHeader "Location" "http://server.example.com/something/788"+    modifyResponse $ setContentType "text/plain"++    b' <- readRequestBody 1024+    writeLBS b'++handlePutWithExpectation :: Snap ()+handlePutWithExpectation = do+    setTimeout 5+    modifyResponse $ setResponseStatus 201 "Created"+    modifyResponse $ setHeader "Cache-Control" "no-cache"+    modifyResponse $ setContentType "text/plain"++    b' <- readRequestBody 1024+    writeLBS b'++handleSizeRequest :: Snap ()+handleSizeRequest = do+    r <- getRequest+    let mm = getHeader "Content-Type" r++    t <- case mm of+        Just m -> return m+        _ -> do+            serveUnsupported+            return ""++    modifyResponse $ setResponseStatus 200 "OK"+    modifyResponse $ setContentType t++    b' <- readRequestBody 65536+    writeBS $ S.pack $ show $ L.length b'++updateResource :: Snap ()+updateResource = do+    bs' <- readRequestBody 4096+    let b' = fromLazy bs'++    im' <- getParam "id"+    let i' = fromMaybe "0" im'++    -- TODO something++    modifyResponse $ setResponseStatus 204 "Updated" -- "No Content"+    modifyResponse $ setHeader "Cache-Control" "no-cache"+    modifyResponse $ setContentLength 0+    return ()+  where+    fromLazy ls' = S.concat $ L.toChunks ls'++serveNotFound :: Snap a+serveNotFound = do+    modifyResponse $ setResponseStatus 404 "Not Found"+    modifyResponse $ setHeader "Content-Type" "text/html"++    writeBS "404 Not Found"++    r <- getResponse+    finishWith r++serveBadRequest :: Snap ()+serveBadRequest = do+    modifyResponse $ setResponseStatus 400 "Bad Request"+    writeBS "400 Bad Request\n"++serveMethodNotAllowed :: Snap ()+serveMethodNotAllowed = do+    modifyResponse $ setResponseStatus 405 "Method Not Allowed"+    modifyResponse $ setHeader "Allow" "GET, POST, PUT"++    writeBS "405 Method Not Allowed\n"+    r <- getResponse+    finishWith r++serveUnsupported :: Snap ()+serveUnsupported = do+    modifyResponse $ setResponseStatus 415 "Unsupported Media Type"+    writeBS "415 Unsupported Media Type\n"+    r <- getResponse+    finishWith r++--+-- The exception will be dumped to the server's stdout, while the supplied+-- message will be sent out with the response (ideally only for debugging+-- purposes, but easier than looking in log/error.log for details).+--++serveError :: ByteString -> SomeException -> Snap ()+serveError x' e = do+    debug msg+    modifyResponse $ setResponseStatus 500 "Internal Server Error"+    writeBS x'+    r <- getResponse+    finishWith r+  where+    msg = show (e :: SomeException)++debug :: String -> Snap ()+debug cs = do+    liftIO $ do+        hPutStrLn stderr ""+        hPutStrLn stderr cs+        hFlush stderr
+ tests/TestSuite.hs view
@@ -0,0 +1,857 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2021 Athae Eredh Siniath and Others+--+-- The code in this file, and the program it is a part of, is made+-- available to you by its authors as open source software: you can+-- redistribute it and/or modify it under a BSD licence.+--+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++{-# OPTIONS -fno-warn-unused-imports #-}++module TestSuite where++import Blaze.ByteString.Builder (Builder)+import qualified Blaze.ByteString.Builder as Builder (+    fromByteString,+    toByteString,+ )+import qualified Blaze.ByteString.Builder.Char8 as Builder (+    fromChar,+    fromString,+ )+import Control.Applicative+import Control.Concurrent (MVar, newEmptyMVar, putMVar, takeMVar)+import Control.Exception (Exception, bracket, handleJust)+import Control.Monad (forM_, guard)+import Data.Aeson (+    FromJSON,+    ToJSON,+    Value (..),+    encode,+    object,+    parseJSON,+    toJSON,+    (.:),+    (.=),+ )+import Data.Aeson.Encode.Pretty+#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.KeyMap as KeyMap+#else+import qualified Data.HashMap.Strict as KeyMap+#endif+import Data.Bits+import Data.Maybe (fromJust)+import Data.Monoid+import Data.String+import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import GHC.Generics hiding (Selector)+import Network.Socket (SockAddr (..))+import Network.URI (URI (..), URIAuth (..), parseURI)+import System.Timeout (timeout)+import Test.HUnit+import Test.Hspec (Spec, describe, it)+import Test.Hspec.Expectations (Selector, anyException, shouldThrow)++import Debug.Trace++--+-- Otherwise redundent imports, but useful for testing in GHCi.+--++import Data.Attoparsec.ByteString.Char8 (Parser, parseOnly, parseTest)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as S+import qualified Data.ByteString.Lazy as L+import Debug.Trace+import System.IO.Streams (InputStream, OutputStream)+import qualified System.IO.Streams as Streams+import qualified System.IO.Streams.Debug as Streams++--+-- what we're actually testing+--++import MockServer (localPort)+import Network.Http.Client+import Network.Http.Connection (Connection (..))+import Network.Http.Inconvenience (+    HttpClientError (..),+    TooManyRedirects (..),+    parseURL,+    splitURI,+ )+import Network.Http.Internal (+    Request (..),+    Response (..),+    composeRequestBytes,+    lookupHeader,+ )+import Network.Http.ResponseParser (readDecimal, readResponseHeader)++localhost = S.pack ("localhost:" ++ show localPort)++suite :: Spec+suite = do+    describe "Opening a connection" $ do+        testConnectionHost++    describe "Request, when serialized" $ do+        testRequestLineFormat+        testRequestTermination+        testEnsureHostField+        testAcceptHeaderFormat+        testBasicAuthorizatonHeader++    describe "Parsing responses" $ do+        testResponseParser1+        testResponseParserMismatch+        testPaddedContentLength+        --      testTrailingWhitespace+        testChunkedEncoding+        testContentLength+        testDevoidOfContent+        testCompressedResponse+        testRepeatedResponseHeaders++    describe "Expectation handling" $ do+        testExpectationContinue++    describe "Convenience API" $ do+        testPutChunks+        testPostChunks+        testPostWithForm+        testGetRedirects+        testSplitURI+        testParseURL+        testParseURLHasEscaped+        testGetLocalRedirects+        testGetFormatsRequest+        testExcessiveRedirects+        testGeneralHandler+        testEstablishConnection+        testParsingJson1+        testParsingJson2+        testPutWithSimple+        testPutWithJson+        testMultipartUpload++    describe "Corner cases in protocol compliance" $ do+        testSendBodyFor PUT+        testSendBodyFor DELETE+        testSendBodyFor PATCH++testRequestTermination =+    it "terminates with a blank line" $ do+        c <- openConnection "localhost" localPort+        let q = buildRequest1 $ do+                http GET "/time"+                setAccept "text/plain"++        let e' = Builder.toByteString $ composeRequestBytes q "booga"+        let n = S.length e' - 4+        let (a', b') = S.splitAt n e'++        assertEqual "Termination not CRLF CRLF" "\r\n\r\n" b'+        assertBool+            "Must be only one blank line at end of headers"+            ('\n' /= S.last a')++        closeConnection c++testRequestLineFormat = do+    it "has a properly formatted request line" $+        bracket+            (fakeConnection)+            (return)+            ( \c -> do+                let q = buildRequest1 $ do+                        http GET "/time"++                let e' = Builder.toByteString $ composeRequestBytes q (cHost c)+                let l' = S.takeWhile (/= '\r') e'++                assertEqual "Invalid HTTP request line" "GET /time HTTP/1.1" l'+            )++    it "handles empty request path" $+        bracket+            (fakeConnection)+            (return)+            ( \c -> do+                let q = buildRequest1 $ do+                        http GET ""++                let e' = Builder.toByteString $ composeRequestBytes q (cHost c)+                let l' = S.takeWhile (/= '\r') e'++                assertEqual "Invalid HTTP request line" "GET / HTTP/1.1" l'+            )++fakeConnection :: IO Connection+fakeConnection = do+    i <- Streams.nullInput+    o <- Streams.nullOutput+    c <- makeConnection "www.example.com" (return ()) o i+    return c++testAcceptHeaderFormat =+    it "properly formats Accept header" $ do+        let q = buildRequest1 $ do+                setAccept' [("text/html", 1), ("*/*", 0.0)]++        let h = qHeaders q+        let (Just a) = lookupHeader h "Accept"+        assertEqual "Failed to format header" "text/html; q=1.0, */*; q=0.0" a++testBasicAuthorizatonHeader =+    it "properly formats Authorization header" $ do+        let q = buildRequest1 $ do+                setAuthorizationBasic "Aladdin" "open sesame"++        let h = qHeaders q+        let (Just a) = lookupHeader h "Authorization"+        assertEqual "Failed to format header" "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" a++testConnectionHost = do+    it "properly caches hostname and port" $ do+        bracket+            (openConnection "localhost" localPort)+            closeConnection+            ( \c -> do+                let h' = cHost c+                assertEqual+                    "Host value needs to be name, not IP address"+                    expected+                    h'+            )+  where+    expected = S.pack $ "localhost:" ++ show localPort++{-+    Incidentally, Host is *not* stored in the Headers map, but is a field+    of the Request object.+-}+testEnsureHostField =+    it "has a properly formatted Host header" $ do+        let q1 = buildRequest1 $ do+                http GET "/hello.txt"++        let h1 = qHost q1+        assertEqual "Incorrect Host header" Nothing h1++        let q2 = buildRequest1 $ do+                http GET "/hello.txt"+                setHostname "other.example.com" 80++        let h2 = qHost q2+        assertEqual "Incorrect Host header" (Just "other.example.com") h2++        let q3 = buildRequest1 $ do+                http GET "/hello.txt"+                setHostname "other.example.com" 54321++        let h3 = qHost q3+        assertEqual "Incorrect Host header" (Just "other.example.com:54321") h3++testResponseParser1 =+    it "parses a simple 200 response" $ do+        p <- Streams.withFileAsInput "tests/example1.txt" (\i -> readResponseHeader i)++        assertEqual "Incorrect parse of response" 200 (getStatusCode p)+        return ()++testResponseParserMismatch =+    it "parses response when HTTP version doesn't match" $ do+        p <- Streams.withFileAsInput "tests/example3.txt" (\i -> readResponseHeader i)++        assertEqual "Incorrect parse of response" 200 (getStatusCode p)+        return ()++testPaddedContentLength =+    it "handles padded Content-Length" $ do+        p <- Streams.withFileAsInput "tests/example4.txt" (\i -> readResponseHeader i)++        let (Just len) = pContentLength p+        assertEqual "Should have trimmed in decimal conversion" 86 len++{-+    Presently inactive+-}+testTrailingWhitespace =+    it "where headers have trailing whitespace" $ do+        p <- Streams.withFileAsInput "tests/example4.txt" (\i -> readResponseHeader i)++        let (Just value) = getHeader p "Content-Length"+        assertEqual "Should have trimmed field value" "86" value++testChunkedEncoding =+    it "recognizes chunked transfer encoding and decodes" $ do+        c <- openConnection "localhost" localPort++        let q = buildRequest1 $ do+                http GET "/time"++        sendRequest c q emptyBody+        receiveResponse+            c+            ( \p i1 -> do+                let cm = getHeader p "Transfer-Encoding"+                assertEqual "Should be chunked encoding!" (Just "chunked") cm++                (i2, getCount) <- Streams.countInput i1+                Streams.skipToEof i2++                len <- getCount+                assertEqual "Incorrect number of bytes read" 29 len+            )++testContentLength = do+    it "recognzies fixed length message" $ do+        c <- openConnection "localhost" localPort++        let q = buildRequest1 $ do+                http GET "/static/statler.jpg"++        sendRequest c q emptyBody++        receiveResponse+            c+            ( \p i1 -> do+                let nm = getHeader p "Content-Length"+                assertMaybe "Should be a Content-Length header!" nm++                let n = read $ S.unpack $ fromJust nm :: Int+                assertEqual "Should be a fixed length message!" 4611 n++                (i2, getCount) <- Streams.countInput i1+                x' <- Streams.readExactly 4611 i2++                len <- getCount+                assertEqual "Incorrect number of bytes read" 4611 len+                assertBool "Incorrect length" (4611 == S.length x')++                end <- Streams.atEOF i2+                assertBool "Expected end of stream" end+            )++    it "doesn't choke if server neglects Content-Length" $ do+        p <- Streams.withFileAsInput "tests/example3.txt" (\i -> readResponseHeader i)++        assertEqual "Incorrect parse of response" 200 (getStatusCode p)+        assertEqual "Incorrect parse of response" Nothing (getHeader p "Content-Length")+        assertEqual "Should not have pContentLength" Nothing (pContentLength p)+        return ()++    it "reads body without Content-Length or Transfer-Encoding" $ do+        c <- fakeConnectionHttp10+        let q = buildRequest1 $ do+                http GET "/fake"+        sendRequest c q emptyBody+        receiveResponse+            c+            ( \_ i1 -> do+                (i2, getCount) <- Streams.countInput i1+                o <- Streams.nullOutput+                Streams.connect i2 o++                end <- Streams.atEOF i2+                assertBool "Expected end of stream" end++                len <- getCount+                assertEqual "Incorrect number of bytes read" 4611 len+            )++        return ()++fakeConnectionHttp10 :: IO Connection+fakeConnectionHttp10 = do+    x' <- S.readFile "tests/example3.txt"+    i <- Streams.fromByteString x'++    o <- Streams.nullOutput+    c <- makeConnection "bad.example.com" (return ()) o i+    return c++{-+    Corner case where servers responding 204 No Content are not required to+    transmit a Content-Length header; Snap *does* send one, so we can't test it+    in the MockServer, so fake it with example5.txt+-}++testDevoidOfContent = do+    it "handles 204 No Content response without Content-Length" $+        timeout_ 2 $ do+            (c, mv) <- fakeConnectionNoContent+            let q = buildRequest1 $ do+                    http GET "/fake"+            sendRequest c q emptyBody+            receiveResponse+                c+                ( \_ i1 -> do+                    (i2, getCount) <- Streams.countInput i1+                    o <- Streams.nullOutput+                    Streams.connect i2 o++                    end <- Streams.atEOF i2+                    assertBool "Expected end of stream" end++                    len <- getCount+                    assertEqual "Incorrect number of bytes read" 0 len+                )+            putMVar mv ()+            return ()+  where+    secs :: Int+    secs = 10 ^ (6 :: Int)++    timeout_ :: Int -> IO a -> IO a+    timeout_ t m = timeout (t * secs) m >>= maybe (error "timeout") return++fakeConnectionNoContent :: IO (Connection, MVar ())+fakeConnectionNoContent = do+    x' <- S.readFile "tests/example5.txt"+    i1 <- Streams.fromByteString x'+    mv <- newEmptyMVar+    i2 <- Streams.makeInputStream $ blockOn mv+    i3 <- Streams.concatInputStreams [i1, i2]++    o <- Streams.nullOutput+    c <- makeConnection "worse.example.com" (return ()) o i3+    return (c, mv)+  where+    blockOn :: MVar () -> IO (Maybe ByteString)+    blockOn mv = takeMVar mv >> return Nothing++{-+    This had to change when we moved to an internal test server; seems+    Snap is doing something funny when gzipping and switching to chunked+    encoding no matter what I do.+-}+testCompressedResponse =+    it "recognizes gzip content encoding and decompresses" $ do+        c <- openConnection "localhost" localPort++        let q = buildRequest1 $ do+                http GET "/static/hello.html"+                setHeader "Accept-Encoding" "gzip"++        sendRequest c q emptyBody++        receiveResponse+            c+            ( \p i -> do+                let nm = getHeader p "Content-Encoding"+                assertMaybe "Should be a Content-Encoding header!" nm+                assertEqual "Content-Encoding header should be 'gzip'!" (Just "gzip") nm++                (i2, getCount) <- Streams.countInput i+                x' <- Streams.readExactly 102 i2++                len <- getCount+                assertEqual "Incorrect number of bytes read" 102 len+                assertBool "Incorrect length" (102 == S.length x')++                end <- Streams.atEOF i+                assertBool "Expected end of stream" end+            )++{-+    This isn't much of a test yet; we really need to test+    a) that 100 Continue was received b) that it was absorbed+    c) that body is correct size, and then d) 4xx and 5xx+    responses are propegated through.+-}++testExpectationContinue =+    it "sends expectation and handles 100 response" $ do+        c <- openConnection "localhost" localPort++        let q = buildRequest1 $ do+                http PUT "/resource/x149"+                setExpectContinue++        sendRequest+            c+            q+            ( \o -> do+                Streams.write (Just (Builder.fromString "Hello world\n")) o+            )++        receiveResponse+            c+            ( \p i -> do+                assertEqual "Incorrect status code" 201 (getStatusCode p)+                x' <- Streams.readExactly 12 i++                end <- Streams.atEOF i+                assertBool "Expected end of stream" end++                assertEqual "Incorrect body" "Hello world\n" x'+            )++        closeConnection c++assertMaybe :: String -> Maybe a -> Assertion+assertMaybe prefix m0 =+    case m0 of+        Nothing -> assertFailure prefix+        Just _ -> assertBool "" True++testPutChunks =+    it "PUT correctly chunks known size entity body" $ do+        let url = S.concat ["http://", localhost, "/size"]++        put url "text/plain" body handler+  where+    body :: OutputStream Builder -> IO ()+    body o = do+        let x = mconcat $ replicate 33000 (Builder.fromChar 'x')+        Streams.write (Just x) o++    handler :: Response -> InputStream ByteString -> IO ()+    handler _ i = do+        (Just b') <- Streams.read i++        end <- Streams.atEOF i+        assertBool "Expected end of stream" end++        let size = readDecimal b' :: Int+        assertEqual "Should have replied with correct file size" 33000 size++testSendBodyFor meth =+    it ("Sends a request body for " ++ show meth) $ do+        c <- openConnection "localhost" localPort++        let q = buildRequest1 $ do+                http meth "/size"+                setContentType "text/plain"+                setTransferEncoding++        sendRequest+            c+            q+            ( \o -> do+                Streams.write (Just (Builder.fromString "a request")) o+            )++        receiveResponse+            c+            ( \p i -> do+                assertEqual "Incorrect status code" 200 (getStatusCode p)+                (Just b') <- Streams.read i++                let size = readDecimal b' :: Int+                assertEqual "Should have received a request body" 9 size+            )++        closeConnection c++testPostChunks =+    it "POST correctly chunks a fileBody" $ do+        let url = S.concat ["http://", localhost, "/size"]++        post url "image/jpeg" (fileBody "tests/statler.jpg") handler+  where+    handler :: Response -> InputStream ByteString -> IO ()+    handler p i = do+        let code = getStatusCode p+        assertEqual "Expected 200 OK" 200 code++        (Just b') <- Streams.read i++        end <- Streams.atEOF i+        assertBool "Expected end of stream" end++        let size = readDecimal b' :: Int+        assertEqual "Should have replied with correct file size" 4611 size++testPostWithForm =+    it "POST with form data correctly encodes parameters" $ do+        let url = S.concat ["http://", localhost, "/postbox"]++        postForm+            url+            [ ("name", "Kermit")+            , ("role", "St&gehand")+            , ("country", Text.encodeUtf8 $ Text.pack "Nørway")+            ]+            handler+  where+    handler :: Response -> InputStream ByteString -> IO ()+    handler p i = do+        let code = getStatusCode p+        assertEqual "Expected 201" 201 code++        b' <- Streams.readExactly 48 i++        end <- Streams.atEOF i+        assertBool "Expected end of stream" end++        assertEqual+            "Incorrect URL encoding"+            "name=Kermit&role=St%26gehand&country=N%c3%b8rway"+            b'++testGetRedirects =+    it "GET internal handler follows redirect on 307" $ do+        let url = S.concat ["http://", localhost, "/bounce"]++        get url handler+  where+    handler :: Response -> InputStream ByteString -> IO ()+    handler p i1 = do+        let code = getStatusCode p+        assertEqual "Should have been final code" 200 code++        (i2, getCount) <- Streams.countInput i1+        Streams.skipToEof i2++        len <- getCount+        assertEqual "Incorrect number of bytes read" 29 len++testGetLocalRedirects =+    it "GET internal handler follows local redirect on 307" $ do+        let url = S.concat ["http://", localhost, "/local"]++        get url handler+  where+    handler :: Response -> InputStream ByteString -> IO ()+    handler p i1 = do+        let code = getStatusCode p+        assertEqual "Should have been final code" 200 code++        (i2, getCount) <- Streams.countInput i1+        Streams.skipToEof i2++        len <- getCount+        assertEqual "Incorrect number of bytes read" 29 len++testSplitURI =+    it "check splitURI for local redirects" $ do+        let a1 = "http://asdf@ya.ru:8000/hello/?asd=asd&abc=2"+            r1 = S.pack "/hello/?asd=asd&abc=2"+        assertEqual "Incorrect split uri 1" (S.pack a1) (splitURI (fromJust $ parseURI a1) r1)++        let a2 = "http://asdf@ya.ru:8000/again/?asd=asd&abc=2"+            r2 = S.pack "/again/?asd=asd&abc=2"+        assertEqual "Incorrect split uri 2" (S.pack a2) (splitURI (fromJust $ parseURI a2) r2)++        let a3 = "http://ya.ru:8000/here/?asd=asd&abc=2"+            r3 = S.pack "/here/?asd=asd&abc=2"+        assertEqual "Incorrect split uri 3" (S.pack a3) (splitURI (fromJust $ parseURI a3) r3)++        let a4 = "http://ya.ru/?asd=asd&abc=2#papa"+            r4 = S.pack "/?asd=asd&abc=2#papa"+        assertEqual "Incorrect split uri 4" (S.pack a4) (splitURI (fromJust $ parseURI a4) r4)++        let a5 = "http://ya.ru/?asd=asd&abc=2#papa"+            r5 = S.pack "http://google.ru/"+        assertEqual "Incorrect split uri 5" r5 (splitURI (fromJust $ parseURI a5) r5)++testParseURL =+    it "Parse URL with chars needing encoding" $ do+        let url = parseURL (Text.encodeUtf8 $ Text.pack "http://example.com/α")+        assertEqual+            "Incorrect URL parsing"+            (URI "http:" (Just $ URIAuth "" "example.com" "") "/%CE%B1" "" "")+            url++testParseURLHasEscaped =+    it "Parse URL with chars already encoded" $ do+        let url = parseURL (Text.encodeUtf8 $ Text.pack "http://example.com/hello%20world")+        assertEqual+            "Incorrect URL parsing"+            (URI "http:" (Just $ URIAuth "" "example.com" "") "/hello%20world" "" "")+            url++testGetFormatsRequest =+    it "GET includes a properly formatted request path" $ do+        let url = S.concat ["http://", localhost]+        x' <- get url concatHandler'++        assertBool "Incorrect context path" (S.length x' > 0)++testExcessiveRedirects =+    it "too many redirects result in an exception" $ do+        let url = S.concat ["http://", localhost, "/loop"]++        get url handler `shouldThrow` tooManyRedirects+  where+    handler :: Response -> InputStream ByteString -> IO ()+    handler _ _ = do+        assertBool "Should have thrown exception before getting here" False++testRepeatedResponseHeaders =+    it "repeated response headers are properly concatonated" $ do+        let url = S.concat ["http://", localhost, "/cookies"]++        get url handler+  where+    handler :: Response -> InputStream ByteString -> IO ()+    handler r _ = do+        assertEqual "Invalid response headers" (Just "stone=diamond,metal=tungsten") (getHeader r "Set-Cookie")++{-+    From http://stackoverflow.com/questions/6147435/is-there-an-assertexception-in-any-of-the-haskell-test-frameworks+    because "although HUnit doesn't have this, it's easy to write your+    own". Uh huh. Surely there's an easier way to do this.+-}++assertException :: (Exception e, Eq e) => e -> IO a -> IO ()+assertException ex action =+    handleJust isWanted (const $ return ()) $ do+        _ <- action+        assertFailure $ "Expected exception: " ++ show ex+  where+    isWanted = guard . (== ex)++testGeneralHandler =+    it "GET with general purpose handler throws exception on 404" $ do+        let url = S.concat ["http://", localhost, "/booga"]++        get url concatHandler' `shouldThrow` httpClientError 404++tooManyRedirects :: Selector TooManyRedirects+tooManyRedirects = const True++--              :: Int -> Selector HttpClientError+httpClientError :: Int -> HttpClientError -> Bool+httpClientError expected (HttpClientError actual _) = expected == actual++testEstablishConnection =+    it "public establish function behaves correctly" $ do+        let url = S.concat ["http://", localhost, "/static/statler.jpg"]++        x' <-+            withConnection (establishConnection url) $+                ( \c -> do+                    let q = buildRequest1 $ do+                            http GET "/static/statler.jpg"+                    -- TODO be nice if we could replace that with 'url';+                    -- fix the routeRequests function in TestServer maybe?+                    sendRequest c q emptyBody+                    receiveResponse c concatHandler'+                )++        let len = S.length x'+        assertEqual "Incorrect number of bytes read" 4611 len++testParsingJson1 =+    it "GET with JSON handler behaves" $ do+        let url = S.concat ["http://", localhost, "/static/data-eu-gdp.json"]++        x <- get url jsonHandler+        let (Object o) = x+        let (Just v) = KeyMap.lookup "label" o+        let (String t) = v++        assertEqual "Incorrect response" "Europe (EU27)" t++testParsingJson2 =+    it "GET with JSON handler parses using Aeson" $ do+        let url = S.concat ["http://", localhost, "/static/data-jp-gdp.json"]++        x <- get url jsonHandler :: IO GrossDomesticProduct++        assertEqual "Incorrect response" "Japan" (gLabel x)+        assertEqual "Data not parsed as expected" 2008 (fst $ last $ gData x)++--      L.putStr $ encodePretty x++{-+    Go to the trouble to create a Haskell data type representing the JSON feed+    we're getting from the sample data files. The Generic trick doesn't work+    because data is a reserved word, of course.+-}++data GrossDomesticProduct = GrossDomesticProduct+    { gLabel :: Text+    , gData :: [(Int, Double)]+    }+    deriving (Show, Generic)++instance FromJSON GrossDomesticProduct where+    parseJSON (Object o) =+        GrossDomesticProduct+            <$> o .: "label"+            <*> o .: "data"+    parseJSON _ = undefined++instance ToJSON GrossDomesticProduct where+    toJSON (GrossDomesticProduct l d) =+        object+            [ "label" .= l+            , "data" .= d+            ]++testPutWithSimple =+    it "PUT with static data" $ do+        let url = S.concat ["http://", localhost, "/resource/y98"]++        x' <- put url "text/plain" (simpleBody b') simpleHandler++        assertEqual+            "Object was encoded to JSON as expected"+            "Hello"+            x'+  where+    b' :: ByteString+    b' = S.pack "Hello"++testPutWithJson =+    it "PUT with json data" $ do+        let url = S.concat ["http://", localhost, "/resource/y99"]++        x' <- put url "application/json" (jsonBody obj) simpleHandler++        assertEqual+            "Object was encoded to JSON as expected"+            "{\"data\":[[2000,1],[2020,0]],\"label\":\"Sealand\"}"+            x'+  where+    obj :: GrossDomesticProduct+    obj =+        GrossDomesticProduct+            { gLabel = "Sealand"+            , gData = [(2000, 1), (2020, 0)]+            }++    obj' :: ByteString+    obj' = L.toStrict (encode obj)++testMultipartUpload =+    it "PUT with json data" $ do+        let url = S.concat ["http://", localhost, "/postbox"]++        let boundary = packBoundary "bEacHV0113YB@ll"++        let q = buildRequest1 $ do+                http POST "/postbox"+                setContentMultipart boundary++        let parts =+                [ simplePart "first" Nothing "Old guys in the box."+                , filePart "second" (Just "text/plain") "tests/hello.txt"+                ]++        c <- openConnection "localhost" localPort++        sendRequest c q (multipartFormBody boundary parts)++        result <- receiveResponse c simpleHandler++        closeConnection c++        expected <- S.readFile "tests/multipart.bin"+        assertEqual+            "Multipart form data was not encoded as expected"+            expected+            result
+ tests/check.hs view
@@ -0,0 +1,25 @@+--+-- HTTP client for use with io-streams+--+-- Copyright © 2012-2014 Operational Dynamics Consulting, Pty Ltd+--+-- The code in this file, and the program it is a part of, is made+-- available to you by its authors as open source software: you can+-- redistribute it and/or modify it under a BSD licence.+--++{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS -fno-warn-unused-imports #-}++module Main where++import OpenSSL (withOpenSSL)+import Test.Hspec (hspec)++import MockServer (runMockServer)+import TestSuite (suite)++main :: IO ()+main = withOpenSSL $ do+    runMockServer+    hspec suite
+ tests/data-eu-gdp.json view
@@ -0,0 +1,4 @@+{+    "label": "Europe (EU27)",+    "data": [[1999, 3.0], [2000, 3.9], [2001, 2.0], [2002, 1.2], [2003, 1.3], [2004, 2.5], [2005, 2.0], [2006, 3.1], [2007, 2.9], [2008, 0.9]]+}
+ tests/data-jp-gdp.json view
@@ -0,0 +1,4 @@+{+    "label": "Japan",+    "data": [[1999, -0.1], [2000, 2.9], [2001, 0.2], [2002, 0.3], [2003, 1.4], [2004, 2.7], [2005, 1.9], [2006, 2.0], [2007, 2.3], [2008, -0.7]]+}
+ tests/data-us-gdp.json view
@@ -0,0 +1,4 @@+{+    "label": "USA",+    "data": [[1999, 4.4], [2000, 3.7], [2001, 0.8], [2002, 1.6], [2003, 2.5], [2004, 3.6], [2005, 2.9], [2006, 2.8], [2007, 2.0], [2008, 1.1]]+}
+ tests/example1.txt view
@@ -0,0 +1,6 @@+HTTP/1.1 200 OK
+Content-Type: text/plain
+Content-Length: 10
+
+Hi there.+
+ tests/example2.txt view
@@ -0,0 +1,17 @@+HTTP/1.1 200 OK
+Content-Type: text/html; charset=UTF-8
+Server: Apache/2.2.22
+Expires: Fri, 04 Jan 2013 06:48:57 GMT
+Date: Fri, 04 Jan 2013 06:48:56 GMT
+Content-Length: 133
+Cache-Control: max-age=1
+
+<html>+<head>+<title>Hello</title>+</head>+<body>+<h1>Hello!</h1>+<p>Hello world. <span class="FIXME">□</span></p>+</body>+</html>
+ tests/example3.txt view

binary file changed (absent → 4675 bytes)

+ tests/example4.txt view
@@ -0,0 +1,6 @@+HTTP/1.1 200 OK
+Content-Type: text/plain
+Content-Length: 86     
+
+This is an HTTP response with a header that has trailing whitespace. Bad+server, Bad!
+ tests/example5.txt view
@@ -0,0 +1,3 @@+HTTP/1.1 204 No Content
+Server: Ye Plain Olde Text File
+
+ tests/hello.html view
@@ -0,0 +1,9 @@+<html>+<head>+<title>Hello</title>+</head>+<body>+<h1>Hello!</h1>+<p>Hello world.</p>+</body>+</html>
+ tests/hello.txt view
@@ -0,0 +1,1 @@+Hello World
+ tests/multipart.bin view
@@ -0,0 +1,12 @@+
+--bEacHV0113YB@ll
+Content-Disposition: form-data; name="first"
+
+Old guys in the box.
+--bEacHV0113YB@ll
+Content-Disposition: form-data; name="second"; filename="hello.txt"
+Content-Type: text/plain
+
+Hello World+
+--bEacHV0113YB@ll--
+ tests/statler.jpg view

binary file changed (absent → 4611 bytes)