connection 0.2.3 → 0.3.1
raw patch · 5 files changed
Files
- LICENSE +1/−1
- Network/Connection.hs +148/−37
- Network/Connection/Types.hs +6/−2
- README.md +56/−54
- connection.cabal +10/−10
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