http-client-tls 0.3.3 → 0.4.0
raw patch · 5 files changed
Files
- ChangeLog.md +71/−0
- Network/HTTP/Client/TLS.hs +142/−45
- bench/Bench.hs +6/−1
- http-client-tls.cabal +15/−9
- test/Spec.hs +59/−1
ChangeLog.md view
@@ -1,3 +1,74 @@+# Changelog for http-client-tls++## 0.4.0++* For MD5 hashes in Base16 format, depend on packages `cryptohash-md5` and+ `base16` rather than `crypton` and `memory` (the latter is unmaintained).++## 0.3.6.4++* data-default-class -> data-default [#546](https://github.com/snoyberg/http-client/pull/546/files)++## 0.3.6.3++* catching up to tls 1.8.0 [#515](https://github.com/snoyberg/http-client/pull/515)++## 0.3.6.2++* Migrate to `crypton`++## 0.3.6.1++* [#482](https://github.com/snoyberg/http-client/issues/482):+ Raise lower bound on `http-client` to fix build.++## 0.3.6++* Allow making requests to raw IPv6 hosts [#477](https://github.com/snoyberg/http-client/pull/477)++## 0.3.5.3++* Fix `newTlsManager` [#325](https://github.com/snoyberg/http-client/issues/325)++## 0.3.5.2++* [#289](https://github.com/snoyberg/http-client/issues/289):+ Keep original `TLSSettings` when creating a `Manager` using `newTlsManagerWith`.++## 0.3.5.1++* Also catch TLSError exceptions [#273](https://github.com/snoyberg/http-client/pull/273)++## 0.3.5++* Add `newTlsManagerWith`+ [#278](https://github.com/snoyberg/http-client/issues/278), which+ provides a variant of `newTlsManager` that takes a `ManagerSettings`+ to base its settings off of.++## 0.3.4.2++* Never throw exceptions on 401 status in `applyDigestAuth`++## 0.3.4.1++* Better exception cleanup behavior++## 0.3.4++* Add 'newTlsManager'+ [#263](https://github.com/snoyberg/http-client/issues/263), which adds+ support for respecting `socks5://` and `socks5h://` `http_proxy` and+ `https_proxy` environment variables.++## 0.3.3.2++* Better handling of internal exceptions++## 0.3.3.1++* Better exception safety via `bracketOnError`+ ## 0.3.3 * Add `DigestAuthException` and generalize `applyDigestAuth`
Network/HTTP/Client/TLS.hs view
@@ -11,6 +11,8 @@ tlsManagerSettings , mkManagerSettings , mkManagerSettingsContext+ , newTlsManager+ , newTlsManagerWith -- * Digest authentication , applyDigestAuth , DigestAuthException (..)@@ -21,7 +23,10 @@ , setGlobalManager ) where -import Data.Default.Class+import Control.Applicative ((<|>))+import Control.Arrow (first)+import System.Environment (getEnvironment)+import Data.Default import Network.HTTP.Client hiding (host, port) import Network.HTTP.Client.Internal hiding (host, port) import Control.Exception@@ -36,11 +41,16 @@ import qualified Data.CaseInsensitive as CI import Data.Maybe (fromMaybe, isJust) import Network.HTTP.Types (status401)-import Crypto.Hash (hash, Digest, MD5)+import qualified Crypto.Hash.MD5 as MD5 import Control.Arrow ((***))-import Data.ByteArray.Encoding (convertToBase, Base (Base16))+import Data.Base16.Types (extractBase16)+import Data.ByteString.Base16 (encodeBase16') import Data.Typeable (Typeable) import Control.Monad.Catch (MonadThrow, throwM)+import qualified Data.Map as Map+import qualified Data.Text as T+import Data.Text.Read (decimal)+import qualified Network.URI as U -- | Create a TLS-enabled 'ManagerSettings' with the given 'NC.TLSSettings' and -- 'NC.SockSettings'@@ -62,27 +72,43 @@ -> NC.TLSSettings -> Maybe NC.SockSettings -> ManagerSettings-mkManagerSettingsContext mcontext tls sock = defaultManagerSettings- { managerTlsConnection = getTlsConnection mcontext (Just tls) sock- , managerTlsProxyConnection = getTlsProxyConnection mcontext tls sock+mkManagerSettingsContext mcontext tls sock = mkManagerSettingsContext' defaultManagerSettings mcontext tls sock sock++-- | Internal, allow different SockSettings for HTTP and HTTPS+mkManagerSettingsContext'+ :: ManagerSettings+ -> Maybe NC.ConnectionContext+ -> NC.TLSSettings+ -> Maybe NC.SockSettings -- ^ insecure+ -> Maybe NC.SockSettings -- ^ secure+ -> ManagerSettings+mkManagerSettingsContext' set mcontext tls sockHTTP sockHTTPS = set+ { managerTlsConnection = getTlsConnection mcontext (Just tls) sockHTTPS+ , managerTlsProxyConnection = getTlsProxyConnection mcontext tls sockHTTPS , managerRawConnection =- case sock of+ case sockHTTP of Nothing -> managerRawConnection defaultManagerSettings- Just _ -> getTlsConnection mcontext Nothing sock+ Just _ -> getTlsConnection mcontext Nothing sockHTTP , managerRetryableException = \e -> case () of ()+#if MIN_VERSION_tls(1,8,0)+ | ((fromException e)::(Maybe TLS.TLSException))==Just (TLS.PostHandshake TLS.Error_EOF) -> True+#else | ((fromException e)::(Maybe TLS.TLSError))==Just TLS.Error_EOF -> True+#endif | otherwise -> managerRetryableException defaultManagerSettings e , managerWrapException = \req ->- let wrapper se =- case fromException se of- Just (_ :: IOException) -> se'- Nothing -> case fromException se of- Just TLS.Terminated{} -> se'- Just TLS.HandshakeFailed{} -> se'- Just TLS.ConnectionNotEstablished -> se'- _ -> se+ let wrapper se+ | Just (_ :: IOException) <- fromException se = se'+ | Just (_ :: TLS.TLSException) <- fromException se = se'+#if !MIN_VERSION_tls(1,8,0)+ | Just (_ :: TLS.TLSError) <- fromException se = se'+#endif+ | Just (_ :: NC.LineTooLong) <- fromException se = se'+ | Just (_ :: NC.HostNotResolved) <- fromException se = se'+ | Just (_ :: NC.HostCannotConnect) <- fromException se = se'+ | otherwise = se where se' = toException $ HttpExceptionRequest req $ InternalException se in handle $ throwIO . wrapper@@ -98,14 +124,15 @@ -> IO (Maybe HostAddress -> String -> Int -> IO Connection) getTlsConnection mcontext tls sock = do context <- maybe NC.initConnectionContext return mcontext- return $ \_ha host port -> do- conn <- NC.connectTo context NC.ConnectionParams- { NC.connectionHostname = host+ return $ \_ha host port -> bracketOnError+ (NC.connectTo context NC.ConnectionParams+ { NC.connectionHostname = strippedHostName host , NC.connectionPort = fromIntegral port , NC.connectionUseSecure = tls , NC.connectionUseSocks = sock- }- convertConnection conn+ })+ NC.connectionClose+ convertConnection getTlsProxyConnection :: Maybe NC.ConnectionContext@@ -114,26 +141,26 @@ -> IO (S.ByteString -> (Connection -> IO ()) -> String -> Maybe HostAddress -> String -> Int -> IO Connection) getTlsProxyConnection mcontext tls sock = do context <- maybe NC.initConnectionContext return mcontext- return $ \connstr checkConn serverName _ha host port -> do- --error $ show (connstr, host, port)- conn <- NC.connectTo context NC.ConnectionParams- { NC.connectionHostname = serverName+ return $ \connstr checkConn serverName _ha host port -> bracketOnError+ (NC.connectTo context NC.ConnectionParams+ { NC.connectionHostname = strippedHostName serverName , NC.connectionPort = fromIntegral port , NC.connectionUseSecure = Nothing , NC.connectionUseSocks = case sock of Just _ -> error "Cannot use SOCKS and TLS proxying together"- Nothing -> Just $ NC.OtherProxy host $ fromIntegral port- }-- NC.connectionPut conn connstr- conn' <- convertConnection conn+ Nothing -> Just $ NC.OtherProxy (strippedHostName host) $ fromIntegral port+ })+ NC.connectionClose+ $ \conn -> do+ NC.connectionPut conn connstr+ conn' <- convertConnection conn - checkConn conn'+ checkConn conn' - NC.connectionSetSecure context conn tls+ NC.connectionSetSecure context conn tls - return conn'+ return conn' convertConnection :: NC.Connection -> IO Connection convertConnection conn = makeConnection@@ -144,18 +171,84 @@ -- already closed, and we get a @ResourceVanished@. (NC.connectionClose conn `Control.Exception.catch` \(_ :: IOException) -> return ()) +-- We may decide in the future to just have a global+-- ConnectionContext and use it directly in tlsManagerSettings, at+-- which point this can again be a simple (newManager+-- tlsManagerSettings >>= newIORef). See:+-- https://github.com/snoyberg/http-client/pull/227.+globalConnectionContext :: NC.ConnectionContext+globalConnectionContext = unsafePerformIO NC.initConnectionContext+{-# NOINLINE globalConnectionContext #-}++-- | Load up a new TLS manager with default settings, respecting proxy+-- environment variables.+--+-- @since 0.3.4+newTlsManager :: MonadIO m => m Manager+newTlsManager = liftIO $ do+ env <- getEnvironment+ let lenv = Map.fromList $ map (first $ T.toLower . T.pack) env+ msocksHTTP = parseSocksSettings env lenv "http_proxy"+ msocksHTTPS = parseSocksSettings env lenv "https_proxy"+ settings = mkManagerSettingsContext' defaultManagerSettings (Just globalConnectionContext) def msocksHTTP msocksHTTPS+ settings' = maybe id (const $ managerSetInsecureProxy proxyFromRequest) msocksHTTP+ $ maybe id (const $ managerSetSecureProxy proxyFromRequest) msocksHTTPS+ settings+ newManager settings'++-- | Load up a new TLS manager based upon specified settings,+-- respecting proxy environment variables.+--+-- @since 0.3.5+newTlsManagerWith :: MonadIO m => ManagerSettings -> m Manager+newTlsManagerWith set = liftIO $ do+ env <- getEnvironment+ let lenv = Map.fromList $ map (first $ T.toLower . T.pack) env+ msocksHTTP = parseSocksSettings env lenv "http_proxy"+ msocksHTTPS = parseSocksSettings env lenv "https_proxy"+ settings = mkManagerSettingsContext' set (Just globalConnectionContext) def msocksHTTP msocksHTTPS+ settings' = maybe id (const $ managerSetInsecureProxy proxyFromRequest) msocksHTTP+ $ maybe id (const $ managerSetSecureProxy proxyFromRequest) msocksHTTPS+ settings+ -- We want to keep the original TLS settings that were+ -- passed in. Sadly they aren't available as a record+ -- field on `ManagerSettings`. So instead we grab the+ -- fields that depend on the TLS settings.+ -- https://github.com/snoyberg/http-client/issues/289+ { managerTlsConnection = managerTlsConnection set+ , managerTlsProxyConnection = managerTlsProxyConnection set+ }+ newManager settings'++parseSocksSettings :: [(String, String)] -- ^ original environment+ -> Map.Map T.Text String -- ^ lower-cased keys+ -> T.Text -- ^ env name+ -> Maybe NC.SockSettings+parseSocksSettings env lenv n = do+ str <- lookup (T.unpack n) env <|> Map.lookup n lenv+ let allowedScheme x = x == "socks5:" || x == "socks5h:"+ uri <- U.parseURI str++ guard $ allowedScheme $ U.uriScheme uri+ guard $ null (U.uriPath uri) || U.uriPath uri == "/"+ guard $ null $ U.uriQuery uri+ guard $ null $ U.uriFragment uri++ auth <- U.uriAuthority uri+ port' <-+ case U.uriPort auth of+ "" -> Nothing -- should we use some default?+ ':':rest ->+ case decimal $ T.pack rest of+ Right (p, "") -> Just p+ _ -> Nothing+ _ -> Nothing++ Just $ NC.SockSettingsSimple (U.uriRegName auth) port'+ -- | Evil global manager, to make life easier for the common use case globalManager :: IORef Manager-globalManager = unsafePerformIO $ do- -- We may decide in the future to just have a global- -- ConnectionContext and use it directly in tlsManagerSettings, at- -- which point this can again be a simple (newManager- -- tlsManagerSettings >>= newIORef). See:- -- https://github.com/snoyberg/http-client/pull/227.- context <- NC.initConnectionContext- let settings = mkManagerSettingsContext (Just context) def Nothing- manager <- newManager settings- newIORef manager+globalManager = unsafePerformIO $ newTlsManager >>= newIORef {-# NOINLINE globalManager #-} -- | Get the current global 'Manager'@@ -238,7 +331,7 @@ -> Request -> Manager -> m (n Request)-applyDigestAuth user pass req man = liftIO $ do+applyDigestAuth user pass req0 man = liftIO $ do res <- httpNoBody req man let throw' = throwM . DigestAuthException req res return $ do@@ -269,7 +362,7 @@ -- we always use no qop or qop=auth ha2 = md5 $ S.concat [method req, ":", path req] - md5 bs = convertToBase Base16 (hash bs :: Digest MD5)+ md5 = extractBase16 . encodeBase16' . MD5.hash key = "Authorization" val = S.concat [ "Digest username=\""@@ -299,6 +392,10 @@ , cookieJar = Just $ responseCookieJar res } where+ -- Since we're expecting a non-200 response, ensure we do not+ -- throw exceptions for such responses.+ req = req0 { checkResponse = \_ _ -> return () }+ stripCI x y | CI.mk x == CI.mk (S.take len y) = Just $ S.drop len y | otherwise = Nothing
bench/Bench.hs view
@@ -1,6 +1,11 @@+{-# LANGUAGE CPP #-} module Main where -import Criterion.Main+#if MIN_VERSION_gauge(0, 2, 0)+import Gauge+#else+import Gauge.Main+#endif import Network.HTTP.Client import Network.HTTP.Client.TLS
http-client-tls.cabal view
@@ -1,5 +1,5 @@ name: http-client-tls-version: 0.3.3+version: 0.4.0 synopsis: http-client backend using the connection package and tls library description: Hackage documentation generation is not reliable. For up to date documentation, please see: <https://www.stackage.org/package/http-client-tls>. homepage: https://github.com/snoyberg/http-client@@ -16,19 +16,22 @@ library exposed-modules: Network.HTTP.Client.TLS other-extensions: ScopedTypeVariables- build-depends: base >= 4 && < 5- , data-default-class- , http-client >= 0.5.0- , connection >= 0.2.2+ build-depends: base >= 4.10 && < 5+ , base16 >= 1.0+ , cryptohash-md5+ , data-default+ , http-client >= 0.7.11+ , crypton-connection , network- , tls >= 1.2+ , tls >= 2.1.2 , bytestring , case-insensitive , transformers , http-types- , cryptonite- , memory , exceptions+ , containers+ , text+ , network-uri default-language: Haskell2010 ghc-options: -Wall @@ -42,6 +45,9 @@ , http-client , http-client-tls , http-types+ , crypton-connection+ , data-default+ , tls benchmark benchmark main-is: Bench.hs@@ -49,6 +55,6 @@ hs-source-dirs: bench default-language: Haskell2010 build-depends: base- , criterion+ , gauge , http-client , http-client-tls
test/Spec.hs view
@@ -1,12 +1,28 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} import Test.Hspec+import Network.Connection import Network.HTTP.Client-import Network.HTTP.Client.TLS+import Network.HTTP.Client.TLS hiding (tlsManagerSettings) import Network.HTTP.Types import Control.Monad (join)+import Data.Default+import qualified Network.TLS as TLS main :: IO () main = hspec $ do+ let tlsSettings = def+ -- Since the release of v2.0.0 of the `tls` package , the default value of+ -- the `supportedExtendedMainSecret` parameter `is `RequireEMS`, this means+ -- that all the connections to a server not supporting TLS1.2+EMS will fail.+ -- The badssl.com service does not yet support TLS1.2+EMS connections, so+ -- let's switch to the value `AllowEMS`, ie: TLS1.2 conenctions without EMS.+#if MIN_VERSION_crypton_connection(0,4,0)+ {settingClientSupported = def {TLS.supportedExtendedMainSecret = TLS.AllowEMS}}+#endif++ let tlsManagerSettings = mkManagerSettings tlsSettings Nothing+ it "make a TLS connection" $ do manager <- newManager tlsManagerSettings withResponse "https://httpbin.org/status/418" manager $ \res ->@@ -27,3 +43,45 @@ join (applyDigestAuth "user" "passwd" "http://httpbin.org/" man) `shouldThrow` \(DigestAuthException _ _ det) -> det == UnexpectedStatusCode++ it "BadSSL: expired" $ do+ manager <- newManager tlsManagerSettings+ let action = withResponse "https://expired.badssl.com/" manager (const (return ()))+ action `shouldThrow` anyException++ it "BadSSL: self-signed" $ do+ manager <- newManager tlsManagerSettings+ let action = withResponse "https://self-signed.badssl.com/" manager (const (return ()))+ action `shouldThrow` anyException++ it "BadSSL: wrong.host" $ do+ manager <- newManager tlsManagerSettings+ let action = withResponse "https://wrong.host.badssl.com/" manager (const (return ()))+ action `shouldThrow` anyException++ it "BadSSL: we do have case-insensitivity though" $ do+ manager <- newManager $ tlsManagerSettings+ withResponse "https://BADSSL.COM" manager $ \res ->+ responseStatus res `shouldBe` status200++ -- https://github.com/snoyberg/http-client/issues/289+ it "accepts TLS settings" $ do+ let+ tlsSettings' = tlsSettings+ { settingDisableCertificateValidation = True+ , settingDisableSession = False+ , settingUseServerName = False+ }+ socketSettings = Nothing+ managerSettings = mkManagerSettings tlsSettings' socketSettings+ manager <- newTlsManagerWith managerSettings+ let url = "https://wrong.host.badssl.com"+ request <- parseRequest url+ response <- httpNoBody request manager+ responseStatus response `shouldBe` status200++ it "global supports TLS" $ do+ manager <- getGlobalManager+ request <- parseRequest "https://httpbin.org"+ response <- httpNoBody request manager+ responseStatus response `shouldBe` status200