packages feed

connection 0.2.3 → 0.3.1

raw patch · 5 files changed

Files

LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2012 Vincent Hanquez <vincent@snarc.org>+Copyright (c) 2012-2019 Vincent Hanquez <vincent@snarc.org>  All rights reserved. 
Network/Connection.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -22,6 +23,8 @@      -- * Exceptions     , LineTooLong(..)+    , HostNotResolved(..)+    , HostCannotConnect(..)      -- * Library initialization     , initConnectionContext@@ -29,22 +32,25 @@      -- * Connection operation     , connectFromHandle+    , connectFromSocket     , connectTo     , connectionClose      -- * Sending and receiving data     , connectionGet+    , connectionGetExact     , connectionGetChunk     , connectionGetChunk'     , connectionGetLine+    , connectionWaitForInput     , connectionPut      -- * TLS related operation     , connectionSetSecure     , connectionIsSecure+    , connectionSessionManager     ) where -import Control.Applicative import Control.Concurrent.MVar import Control.Monad (join) import qualified Control.Exception as E@@ -55,9 +61,11 @@  import System.X509 (getSystemCertificateStore) -import Network.Socks5-import qualified Network as N+import Network.Socks5 (defaultSocksConf, socksConnectWithSocket, SocksAddress(..), SocksHostAddress(..))+import Network.Socket+import qualified Network.Socket.ByteString as N +import Data.Tuple (swap) import Data.Default.Class import Data.Data import Data.ByteString (ByteString)@@ -65,9 +73,8 @@ import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Lazy as L -import qualified Crypto.Random.AESCtr as RNG- import System.Environment+import System.Timeout import System.IO import qualified Data.Map as M @@ -79,7 +86,15 @@ -- the line in ConnectionGetLine. data LineTooLong = LineTooLong deriving (Show,Typeable) +-- | Exception raised when there's no resolution for a specific host+data HostNotResolved = HostNotResolved String deriving (Show,Typeable)++-- | Exception raised when the connect failed+data HostCannotConnect = HostCannotConnect String [E.IOException] deriving (Show,Typeable)+ instance E.Exception LineTooLong+instance E.Exception HostNotResolved+instance E.Exception HostCannotConnect  connectionSessionManager :: Manager -> TLS.SessionManager connectionSessionManager mvar = TLS.SessionManager@@ -87,6 +102,10 @@     , TLS.sessionEstablish  = \sessionID sessionData ->                                modifyMVar_ mvar (return . M.insert sessionID sessionData)     , TLS.sessionInvalidate = \sessionID -> modifyMVar_ mvar (return . M.delete sessionID)+#if MIN_VERSION_tls(1,5,0)+    , TLS.sessionResumeOnlyOnce = \sessionID ->+         modifyMVar mvar (pure . swap . M.updateLookupWithKey (\_ _ -> Nothing) sessionID)+#endif     }  -- | Initialize the library with shared parameters between connection.@@ -98,7 +117,7 @@ makeTLSParams :: ConnectionContext -> ConnectionID -> TLSSettings -> TLS.ClientParams makeTLSParams cg cid ts@(TLSSettingsSimple {}) =     (TLS.defaultParamsClient (fst cid) portString)-        { TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_all }+        { TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_default }         , TLS.clientShared    = def             { TLS.sharedCAStore         = globalCertificateStore cg             , TLS.sharedValidationCache = validationCache@@ -137,46 +156,119 @@           withSecurity (Just tlsSettings) = tlsEstablish h (makeTLSParams cg cid tlsSettings) >>= connectionNew cid . ConnectionTLS           cid = (connectionHostname p, connectionPort p) +-- | Use an already established handle to create a connection object.+--+-- if the TLS Settings is set, it will do the handshake with the server.+-- The SOCKS settings have no impact here, as the handle is already established+connectFromSocket :: ConnectionContext+                  -> Socket+                  -> ConnectionParams+                  -> IO Connection+connectFromSocket cg sock p = withSecurity (connectionUseSecure p)+    where withSecurity Nothing            = connectionNew cid $ ConnectionSocket sock+          withSecurity (Just tlsSettings) = tlsEstablish sock (makeTLSParams cg cid tlsSettings) >>= connectionNew cid . ConnectionTLS+          cid = (connectionHostname p, connectionPort p)+ -- | connect to a destination using the parameter connectTo :: ConnectionContext -- ^ The global context of this connection.           -> ConnectionParams  -- ^ The parameters for this connection (where to connect, and such).           -> IO Connection     -- ^ The new established connection on success. connectTo cg cParams = do-    conFct <- getConFct (connectionUseSocks cParams)-    h      <- conFct (connectionHostname cParams) (N.PortNumber $ connectionPort cParams)-    connectFromHandle cg h cParams+    let conFct = doConnect (connectionUseSocks cParams)+                           (connectionHostname cParams)+                           (connectionPort cParams)+    E.bracketOnError conFct (close . fst) $ \(h, _) ->+        connectFromSocket cg h cParams   where-        getConFct Nothing                            = return N.connectTo-        getConFct (Just (OtherProxy h p))            = return $ \_ _ -> N.connectTo h (N.PortNumber p)-        getConFct (Just (SockSettingsSimple h p))    = return $ socksConnectTo h (N.PortNumber p)-        getConFct (Just (SockSettingsEnvironment v)) = do-            -- if we can't get the environment variable or that the variable cannot be parsed-            -- we connect directly.-            let name = maybe "SOCKS_SERVER" id v-            evar <- E.try (getEnv name)-            case evar of-                Left (_ :: E.IOException) -> return N.connectTo-                Right var                 ->-                    case parseSocks var of-                        Nothing             -> return N.connectTo-                        Just (sHost, sPort) -> return $ socksConnectTo sHost (N.PortNumber $ fromIntegral (sPort :: Int))+    sockConnect sockHost sockPort h p = do+        (sockServ, servAddr) <- resolve' sockHost sockPort+        let sockConf = defaultSocksConf servAddr+        let destAddr = SocksAddress (SocksAddrDomainName $ BC.pack h) p+        (dest, _) <- socksConnectWithSocket sockServ sockConf destAddr+        case dest of+            SocksAddrIPV4 h4 -> return (sockServ, SockAddrInet p h4)+            SocksAddrIPV6 h6 -> return (sockServ, SockAddrInet6 p 0 h6 0)+            SocksAddrDomainName _ -> error "internal error: socks connect return a resolved address as domain name" -        -- Try to parse "host:port" or "host"-        parseSocks s =-            case break (== ':') s of-                (sHost, "")        -> Just (sHost, 1080)-                (sHost, ':':portS) ->-                    case reads portS of-                        [(sPort,"")] -> Just (sHost, sPort)-                        _            -> Nothing-                _                  -> Nothing +    doConnect proxy h p =+        case proxy of+            Nothing                 -> resolve' h p+            Just (OtherProxy proxyHost proxyPort) -> resolve' proxyHost proxyPort+            Just (SockSettingsSimple sockHost sockPort) ->+                sockConnect sockHost sockPort h p+            Just (SockSettingsEnvironment envName) -> do+                -- if we can't get the environment variable or that the string cannot be parsed+                -- we connect directly.+                let name = maybe "SOCKS_SERVER" id envName+                evar <- E.try (getEnv name)+                case evar of+                    Left (_ :: E.IOException) -> resolve' h p+                    Right var                 ->+                        case parseSocks var of+                            Nothing                   -> resolve' h p+                            Just (sockHost, sockPort) -> sockConnect sockHost sockPort h p++    -- Try to parse "host:port" or "host"+    -- if port is ommited then the default SOCKS port (1080) is assumed+    parseSocks :: String -> Maybe (String, PortNumber)+    parseSocks s =+        case break (== ':') s of+            (sHost, "")        -> Just (sHost, 1080)+            (sHost, ':':portS) ->+                case reads portS of+                    [(sPort,"")] -> Just (sHost, sPort)+                    _            -> Nothing+            _                  -> Nothing++    -- Try to resolve the host/port into an address (zero to many of them), then+    -- try to connect from the first address to the last, returning the first one that+    -- succeed+    resolve' :: String -> PortNumber -> IO (Socket, SockAddr)+    resolve' host port = do+        let hints = defaultHints { addrFlags = [AI_ADDRCONFIG], addrSocketType = Stream }+        addrs <- getAddrInfo (Just hints) (Just host) (Just $ show port)+        firstSuccessful $ map tryToConnect addrs+      where+        tryToConnect addr =+            E.bracketOnError+                (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))+                (close)+                (\sock -> connect sock (addrAddress addr) >> return (sock, addrAddress addr))+        firstSuccessful = go []+          where+            go :: [E.IOException] -> [IO a] -> IO a+            go []      [] = E.throwIO $ HostNotResolved host+            go l@(_:_) [] = E.throwIO $ HostCannotConnect host l+            go acc     (act:followingActs) = do+                er <- E.try act+                case er of+                    Left err -> go (err:acc) followingActs+                    Right r  -> return r+ -- | Put a block of data in the connection. connectionPut :: Connection -> ByteString -> IO () connectionPut connection content = withBackend doWrite connection     where doWrite (ConnectionStream h) = B.hPut h content >> hFlush h+          doWrite (ConnectionSocket s) = N.sendAll s content           doWrite (ConnectionTLS ctx)  = TLS.sendData ctx $ L.fromChunks [content] +-- | Get exact count of bytes from a connection.+--+-- The size argument is the exact amount that must be returned to the user.+-- The call will wait until all data is available.  Hence, it behaves like+-- 'B.hGet'.+--+-- On end of input, 'connectionGetExact' will throw an 'E.isEOFError'+-- exception.+connectionGetExact :: Connection -> Int -> IO ByteString+connectionGetExact conn x = loop B.empty 0+  where loop bs y+          | y == x = return bs+          | otherwise = do+            next <- connectionGet conn (x - y)+            loop (B.append bs next) (y + (B.length next))+ -- | Get some bytes from a connection. -- -- The size argument is just the maximum that could be returned to the user.@@ -201,6 +293,19 @@ connectionGetChunk' :: Connection -> (ByteString -> (a, ByteString)) -> IO a connectionGetChunk' = connectionGetChunkBase "connectionGetChunk'" +-- | Wait for input to become available on a connection.+--+-- As with 'hWaitForInput', the timeout value is given in milliseconds.  If the+-- timeout value is less than zero, then 'connectionWaitForInput' waits+-- indefinitely.+--+-- Unlike 'hWaitForInput', this function does not do any decoding, so it+-- returns true when there is /any/ available input, not just full characters.+connectionWaitForInput :: Connection -> Int -> IO Bool+connectionWaitForInput conn timeout_ms = maybe False (const True) <$> timeout timeout_ns tryGetChunk+  where tryGetChunk = connectionGetChunkBase "connectionWaitForInput" conn $ \buf -> ((), buf)+        timeout_ns  = timeout_ms * 1000+ connectionGetChunkBase :: String -> Connection -> (ByteString -> (a, ByteString)) -> IO a connectionGetChunkBase loc conn f =     modifyMVar (connectionBuffer conn) $ \m ->@@ -216,6 +321,7 @@                   updateBuf buf   where     getMoreData (ConnectionTLS tlsctx) = TLS.recvData tlsctx+    getMoreData (ConnectionSocket sock) = N.recv sock 1500     getMoreData (ConnectionStream h)   = B.hGetSome h (16 * 1024)      updateBuf buf = case f buf of (a, !buf') -> return (Just buf', a)@@ -263,7 +369,7 @@       join $ connectionGetChunkBase loc conn $ \s ->         if B.null s           then (eofK, B.empty)-          else case B.breakByte 10 s of+          else case B.break (== 10) s of                  (a, b)                    | B.null b  -> (moreK a, B.empty)                    | otherwise -> (doneK a, B.tail b)@@ -279,9 +385,12 @@ -- | Close a connection. connectionClose :: Connection -> IO () connectionClose = withBackend backendClose-    where backendClose (ConnectionTLS ctx)  = TLS.bye ctx >> TLS.contextClose ctx+    where backendClose (ConnectionTLS ctx)  = ignoreIOExc (TLS.bye ctx) `E.finally` TLS.contextClose ctx+          backendClose (ConnectionSocket sock) = close sock           backendClose (ConnectionStream h) = hClose h +          ignoreIOExc action = action `E.catch` \(_ :: E.IOException) -> return ()+ -- | Activate secure layer using the parameters specified. -- -- This is typically used to negociate a TLS channel on an already@@ -300,17 +409,19 @@         case backend of             (ConnectionStream h) -> do ctx <- tlsEstablish h (makeTLSParams cg (connectionID connection) params)                                        return (ConnectionTLS ctx, Just B.empty)+            (ConnectionSocket s) -> do ctx <- tlsEstablish s (makeTLSParams cg (connectionID connection) params)+                                       return (ConnectionTLS ctx, Just B.empty)             (ConnectionTLS _)    -> return (backend, b)  -- | Returns if the connection is establish securely or not. connectionIsSecure :: Connection -> IO Bool connectionIsSecure conn = withBackend isSecure conn     where isSecure (ConnectionStream _) = return False+          isSecure (ConnectionSocket _) = return False           isSecure (ConnectionTLS _)    = return True -tlsEstablish :: Handle -> TLS.ClientParams -> IO TLS.Context+tlsEstablish :: TLS.HasBackend backend => backend -> TLS.ClientParams -> IO TLS.Context tlsEstablish handle tlsParams = do-    rng <- RNG.makeSystem-    ctx <- TLS.contextNew handle tlsParams rng+    ctx <- TLS.contextNew handle tlsParams     TLS.handshake ctx     return ctx
Network/Connection/Types.hs view
@@ -16,15 +16,19 @@ import Data.X509.CertificateStore import Data.ByteString (ByteString) -import Network.BSD (HostName)-import Network.Socket (PortNumber)+import Network.Socket (PortNumber, Socket) import qualified Network.TLS as TLS  import System.IO (Handle)  -- | Simple backend enumeration, either using a raw connection or a tls connection. data ConnectionBackend = ConnectionStream Handle+                       | ConnectionSocket Socket                        | ConnectionTLS TLS.Context+++-- | Hostname This could either be a name string (punycode encoded) or an ipv4/ipv6+type HostName = String  -- | Connection Parameters to establish a Connection. --
README.md view
@@ -14,68 +14,70 @@  Connect to www.example.com on port 4567 (without socks or tls), then send a byte, receive a single byte, print it, and close the connection:--    import qualified Data.ByteString as B-    import Network.Connection-    import Data.Default--    main = do-        ctx <- initConnectionContext-        con <- connectTo ctx $ ConnectionParams-                                  { connectionHostname  = "www.example.com"-                                  , connectionPort      = 4567-                                  , connectionUseSecure = Nothing-                                  , connectionUseSocks  = Nothing-                                  }-        connectionPut con (B.singleton 0xa)-        r <- connectionGet con 1-        putStrLn $ show r-        connectionClose con+```haskell+import qualified Data.ByteString as B+import Network.Connection+import Data.Default +main = do+    ctx <- initConnectionContext+    con <- connectTo ctx $ ConnectionParams+                              { connectionHostname  = "www.example.com"+                              , connectionPort      = 4567+                              , connectionUseSecure = Nothing+                              , connectionUseSocks  = Nothing+                              }+    connectionPut con (B.singleton 0xa)+    r <- connectionGet con 1+    putStrLn $ show r+    connectionClose con+``` Using a socks proxy is easy, we just need replacing the connectionSocks parameter, for example connecting to the same host, but using a socks proxy at localhost:1080:--    con <- connectTo ctx $ ConnectionParams-                           { connectionHostname  = "www.example.com"-                           , connectionPort      = 4567-                           , connectionUseSecure = Nothing-                           , connectionUseSocks  = Just $ SockSettingsSimple "localhost" 1080-                           }-+```haskell+con <- connectTo ctx $ ConnectionParams+                       { connectionHostname  = "www.example.com"+                       , connectionPort      = 4567+                       , connectionUseSecure = Nothing+                       , connectionUseSocks  = Just $ SockSettingsSimple "localhost" 1080+                       }+``` Connecting to a SSL style socket is equally easy, and need to set the UseSecure fields in ConnectionParams:--    con <- connectTo ctx $ ConnectionParams-                           { connectionHostname  = "www.example.com"-                           , connectionPort      = 4567-                           , connectionUseSecure = Just def-                           , connectionUseSocks  = Nothing-                           }-+```haskell+con <- connectTo ctx $ ConnectionParams+                       { connectionHostname  = "www.example.com"+                       , connectionPort      = 4567+                       , connectionUseSecure = Just def+                       , connectionUseSocks  = Nothing+                       }+``` And finally, you can start TLS in the middle of an insecure connection. This is great for protocol using STARTTLS (e.g. IMAP, SMTP): -    {-# LANGUAGE OverloadedStrings #-}-    import qualified Data.ByteString as B-    import Data.ByteString.Char8 ()-    import Network.Connection-    import Data.Default+```haskell+{-# LANGUAGE OverloadedStrings #-}+import qualified Data.ByteString as B+import Data.ByteString.Char8 ()+import Network.Connection+import Data.Default -    main = do-        ctx <- initConnectionContext-        con <- connectTo ctx $ ConnectionParams-                                  { connectionHostname  = "www.example.com"-                                  , connectionPort      = 4567-                                  , connectionUseSecure = Nothing-                                  , connectionUseSocks  = Nothing-                                  }-        -- talk to the other side with no TLS: says hello and starttls-        connectionPut con "HELLO\n"-        connectionPut con "STARTTLS\n"+main = do+    ctx <- initConnectionContext+    con <- connectTo ctx $ ConnectionParams+                              { connectionHostname  = "www.example.com"+                              , connectionPort      = 4567+                              , connectionUseSecure = Nothing+                              , connectionUseSocks  = Nothing+                              }+    -- talk to the other side with no TLS: says hello and starttls+    connectionPut con "HELLO\n"+    connectionPut con "STARTTLS\n" -        -- switch to TLS-        connectionSetSecure ctx con def+    -- switch to TLS+    connectionSetSecure ctx con def -        -- the connection is from now on using TLS, we can send secret for example-        connectionPut con "PASSWORD 123\n"-        connectionClose con+    -- the connection is from now on using TLS, we can send secret for example+    connectionPut con "PASSWORD 123\n"+    connectionClose con+```
connection.cabal view
@@ -1,5 +1,5 @@ Name:                connection-Version:             0.2.3+Version:             0.3.1 Description:     Simple network library for all your connection need.     .@@ -17,22 +17,22 @@ Category:            Network stability:           experimental Cabal-Version:       >=1.6-Homepage:            http://github.com/vincenthz/hs-connection+Homepage:            https://github.com/vincenthz/hs-connection extra-source-files:  README.md                      CHANGELOG.md  Library   Build-Depends:     base >= 3 && < 5+                   , basement                    , bytestring                    , containers                    , data-default-class-                   , network >= 2.3-                   , tls >= 1.2-                   , cprng-aes-                   , socks >= 0.4-                   , x509 >= 1.4-                   , x509-store >= 1.4-                   , x509-system >= 1.4+                   , network >= 2.6.3+                   , tls >= 1.4+                   , socks >= 0.6+                   , x509 >= 1.5+                   , x509-store >= 1.5+                   , x509-system >= 1.5                    , x509-validation >= 1.5   Exposed-modules:   Network.Connection   Other-modules:     Network.Connection.Types@@ -40,4 +40,4 @@  source-repository head   type: git-  location: git://github.com/vincenthz/hs-connection+  location: https://github.com/vincenthz/hs-connection