{-# LANGUAGE TypeSynonymInstances #-} ----------------------------------------------------------------------------- -- | -- Module : Network.TCP -- Copyright : See LICENSE file -- License : BSD -- -- Maintainer : Ganesh Sittampalam -- Stability : experimental -- Portability : non-portable (not tested) -- -- Some utility functions for working with the Haskell @network@ package. Mostly -- for internal use by the @Network.HTTP@ code, but -- ----------------------------------------------------------------------------- module Network.TCP ( Connection , EndPoint(..) , openTCPPort , isConnectedTo , openTCPConnection , socketConnection , isTCPConnectedTo , HandleStream , HStream(..) , StreamHooks(..) , nullHooks , setStreamHooks , getStreamHooks , hstreamToConnection ) where import Network.BSD (getHostByName, hostAddresses) import Network.Socket ( Socket, SockAddr(SockAddrInet), SocketOption(KeepAlive) , SocketType(Stream), inet_addr, connect , shutdown, ShutdownCmd(..) , sClose, setSocketOption, getPeerName , socket, Family(AF_INET) ) import qualified Network.Stream as Stream ( Stream(readBlock, readLine, writeBlock, close, closeOnEnd) ) import Network.Stream ( ConnError(..) , Result , failWith , failMisc ) import Network.BufferType import Network.HTTP.Base ( catchIO ) import Network.Socket ( socketToHandle ) import Data.Char ( toLower ) import Data.Word ( Word8 ) import Control.Concurrent import Control.Monad ( liftM, when ) import System.IO ( Handle, hFlush, IOMode(..), hClose ) import System.IO.Error ( isEOFError ) import qualified Data.ByteString as Strict import qualified Data.ByteString.Lazy as Lazy ----------------------------------------------------------------- ------------------ TCP Connections ------------------------------ ----------------------------------------------------------------- -- | The 'Connection' newtype is a wrapper that allows us to make -- connections an instance of the Stream class, without GHC extensions. -- While this looks sort of like a generic reference to the transport -- layer it is actually TCP specific, which can be seen in the -- implementation of the 'Stream Connection' instance. newtype Connection = Connection (HandleStream String) newtype HandleStream a = HandleStream {getRef :: MVar (Conn a)} data EndPoint = EndPoint { epHost :: String, epPort :: Int } instance Eq EndPoint where EndPoint host1 port1 == EndPoint host2 port2 = map toLower host1 == map toLower host2 && port1 == port2 data Conn a = MkConn { connSock :: ! Socket , connHandle :: Handle , connBuffer :: BufferOp a , connInput :: Maybe a , connEndPoint :: EndPoint , connHooks :: Maybe (StreamHooks a) , connCloseEOF :: Bool -- True => close socket upon reaching end-of-stream. } | ConnClosed deriving(Eq) hstreamToConnection :: HandleStream String -> Connection hstreamToConnection h = Connection h connHooks' :: Conn a -> Maybe (StreamHooks a) connHooks' ConnClosed{} = Nothing connHooks' x = connHooks x -- all of these are post-op hooks data StreamHooks ty = StreamHooks { hook_readLine :: (ty -> String) -> Result ty -> IO () , hook_readBlock :: (ty -> String) -> Int -> Result ty -> IO () , hook_writeBlock :: (ty -> String) -> ty -> Result () -> IO () , hook_close :: IO () , hook_name :: String -- hack alert: name of the hook itself. } instance Eq ty => Eq (StreamHooks ty) where (==) _ _ = True nullHooks :: StreamHooks ty nullHooks = StreamHooks { hook_readLine = \ _ _ -> return () , hook_readBlock = \ _ _ _ -> return () , hook_writeBlock = \ _ _ _ -> return () , hook_close = return () , hook_name = "" } setStreamHooks :: HandleStream ty -> StreamHooks ty -> IO () setStreamHooks h sh = modifyMVar_ (getRef h) (\ c -> return c{connHooks=Just sh}) getStreamHooks :: HandleStream ty -> IO (Maybe (StreamHooks ty)) getStreamHooks h = readMVar (getRef h) >>= return.connHooks -- | @HStream@ overloads the use of 'HandleStream's, letting you -- overload the handle operations over the type that is communicated -- across the handle. It comes in handy for @Network.HTTP@ 'Request' -- and 'Response's as the payload representation isn't fixed, but overloaded. -- -- The library comes with instances for @ByteString@s and @String@, but -- should you want to plug in your own payload representation, defining -- your own @HStream@ instance _should_ be all that it takes. -- class BufferType bufType => HStream bufType where openStream :: String -> Int -> IO (HandleStream bufType) openSocketStream :: String -> Int -> Socket -> IO (HandleStream bufType) readLine :: HandleStream bufType -> IO (Result bufType) readBlock :: HandleStream bufType -> Int -> IO (Result bufType) writeBlock :: HandleStream bufType -> bufType -> IO (Result ()) close :: HandleStream bufType -> IO () closeQuick :: HandleStream bufType -> IO () closeOnEnd :: HandleStream bufType -> Bool -> IO () instance HStream Strict.ByteString where openStream = openTCPConnection openSocketStream = socketConnection readBlock c n = readBlockBS c n readLine c = readLineBS c writeBlock c str = writeBlockBS c str close c = closeIt c Strict.null True closeQuick c = closeIt c Strict.null False closeOnEnd c f = closeEOF c f instance HStream Lazy.ByteString where openStream = \ a b -> openTCPConnection_ a b True openSocketStream = \ a b c -> socketConnection_ a b c True readBlock c n = readBlockBS c n readLine c = readLineBS c writeBlock c str = writeBlockBS c str close c = closeIt c Lazy.null True closeQuick c = closeIt c Lazy.null False closeOnEnd c f = closeEOF c f instance Stream.Stream Connection where readBlock (Connection c) = Network.TCP.readBlock c readLine (Connection c) = Network.TCP.readLine c writeBlock (Connection c) = Network.TCP.writeBlock c close (Connection c) = Network.TCP.close c closeOnEnd (Connection c) f = Network.TCP.closeEOF c f instance HStream String where openStream = openTCPConnection openSocketStream = socketConnection readBlock ref n = readBlockBS ref n -- This function uses a buffer, at this time the buffer is just 1000 characters. -- (however many bytes this is is left to the user to decypher) readLine ref = readLineBS ref -- The 'Connection' object allows no outward buffering, -- since in general messages are serialised in their entirety. writeBlock ref str = writeBlockBS ref str -- (stringToBuf str) -- Closes a Connection. Connection will no longer -- allow any of the other Stream functions. Notice that a Connection may close -- at any time before a call to this function. This function is idempotent. -- (I think the behaviour here is TCP specific) close c = closeIt c null True -- Closes a Connection without munching the rest of the stream. closeQuick c = closeIt c null False closeOnEnd c f = closeEOF c f -- | @openTCPPort uri port@ establishes a connection to a remote -- host, using 'getHostByName' which possibly queries the DNS system, hence -- may trigger a network connection. openTCPPort :: String -> Int -> IO Connection openTCPPort uri port = openTCPConnection uri port >>= return.Connection -- Add a "persistent" option? Current persistent is default. -- Use "Result" type for synchronous exception reporting? openTCPConnection :: BufferType ty => String -> Int -> IO (HandleStream ty) openTCPConnection uri port = openTCPConnection_ uri port False openTCPConnection_ :: BufferType ty => String -> Int -> Bool -> IO (HandleStream ty) openTCPConnection_ uri port stashInput = withSocket $ \s -> do setSocketOption s KeepAlive 1 hostA <- getHostAddr uri let a = SockAddrInet (toEnum port) hostA connect s a socketConnection_ uri port s stashInput where withSocket action = do s <- socket AF_INET Stream 6 catchIO (action s) (\e -> sClose s >> ioError e) getHostAddr h = do catchIO (inet_addr uri) -- handles ascii IP numbers (\ _ -> do host <- getHostByName_safe uri case hostAddresses host of [] -> fail ("openTCPConnection: no addresses in host entry for " ++ show h) (ha:_) -> return ha) getHostByName_safe h = catchIO (getHostByName h) (\ _ -> fail ("openTCPConnection: host lookup failure for " ++ show h)) -- | @socketConnection@, like @openConnection@ but using a pre-existing 'Socket'. socketConnection :: BufferType ty => String -> Int -> Socket -> IO (HandleStream ty) socketConnection hst port sock = socketConnection_ hst port sock False -- Internal function used to control the on-demand streaming of input -- for /lazy/ streams. socketConnection_ :: BufferType ty => String -> Int -> Socket -> Bool -> IO (HandleStream ty) socketConnection_ hst port sock stashInput = do h <- socketToHandle sock ReadWriteMode mb <- case stashInput of { True -> liftM Just $ buf_hGetContents bufferOps h; _ -> return Nothing } let conn = MkConn { connSock = sock , connHandle = h , connBuffer = bufferOps , connInput = mb , connEndPoint = EndPoint hst port , connHooks = Nothing , connCloseEOF = False } v <- newMVar conn return (HandleStream v) closeConnection :: HStream a => HandleStream a -> IO Bool -> IO () closeConnection ref readL = do -- won't hold onto the lock for the duration -- we are draining it...ToDo: have Connection -- into a shutting-down state so that other -- threads will simply back off if/when attempting -- to also close it. c <- readMVar (getRef ref) closeConn c `catchIO` (\_ -> return ()) modifyMVar_ (getRef ref) (\ _ -> return ConnClosed) where -- Be kind to peer & close gracefully. closeConn ConnClosed = return () closeConn conn = do let sk = connSock conn hFlush (connHandle conn) shutdown sk ShutdownSend suck readL hClose (connHandle conn) shutdown sk ShutdownReceive sClose sk suck :: IO Bool -> IO () suck rd = do f <- rd if f then return () else suck rd -- | Checks both that the underlying Socket is connected -- and that the connection peer matches the given -- host name (which is recorded locally). isConnectedTo :: Connection -> EndPoint -> IO Bool isConnectedTo (Connection conn) endPoint = do v <- readMVar (getRef conn) case v of ConnClosed -> print "aa" >> return False _ | connEndPoint v == endPoint -> catch (getPeerName (connSock v) >> return True) (const $ return False) | otherwise -> return False isTCPConnectedTo :: HandleStream ty -> EndPoint -> IO Bool isTCPConnectedTo conn endPoint = do v <- readMVar (getRef conn) case v of ConnClosed -> return False _ | connEndPoint v == endPoint -> catch (getPeerName (connSock v) >> return True) (const $ return False) | otherwise -> return False readBlockBS :: HStream a => HandleStream a -> Int -> IO (Result a) readBlockBS ref n = onNonClosedDo ref $ \ conn -> do x <- bufferGetBlock ref n maybe (return ()) (\ h -> hook_readBlock h (buf_toStr $ connBuffer conn) n x) (connHooks' conn) return x -- This function uses a buffer, at this time the buffer is just 1000 characters. -- (however many bytes this is is left for the user to decipher) readLineBS :: HStream a => HandleStream a -> IO (Result a) readLineBS ref = onNonClosedDo ref $ \ conn -> do x <- bufferReadLine ref maybe (return ()) (\ h -> hook_readLine h (buf_toStr $ connBuffer conn) x) (connHooks' conn) return x -- The 'Connection' object allows no outward buffering, -- since in general messages are serialised in their entirety. writeBlockBS :: HandleStream a -> a -> IO (Result ()) writeBlockBS ref b = onNonClosedDo ref $ \ conn -> do x <- bufferPutBlock (connBuffer conn) (connHandle conn) b maybe (return ()) (\ h -> hook_writeBlock h (buf_toStr $ connBuffer conn) b x) (connHooks' conn) return x closeIt :: HStream ty => HandleStream ty -> (ty -> Bool) -> Bool -> IO () closeIt c p b = do closeConnection c (if b then readLineBS c >>= \ x -> case x of { Right xs -> return (p xs); _ -> return True} else return True) conn <- readMVar (getRef c) maybe (return ()) (hook_close) (connHooks' conn) closeEOF :: HandleStream ty -> Bool -> IO () closeEOF c flg = modifyMVar_ (getRef c) (\ co -> return co{connCloseEOF=flg}) bufferGetBlock :: HStream a => HandleStream a -> Int -> IO (Result a) bufferGetBlock ref n = onNonClosedDo ref $ \ conn -> do case connInput conn of Just c -> do let (a,b) = buf_splitAt (connBuffer conn) n c modifyMVar_ (getRef ref) (\ co -> return co{connInput=Just b}) return (return a) _ -> do Prelude.catch (buf_hGet (connBuffer conn) (connHandle conn) n >>= return.return) (\ e -> if isEOFError e then do when (connCloseEOF conn) $ catch (closeQuick ref) (\ _ -> return ()) return (return (buf_empty (connBuffer conn))) else return (failMisc (show e))) bufferPutBlock :: BufferOp a -> Handle -> a -> IO (Result ()) bufferPutBlock ops h b = Prelude.catch (buf_hPut ops h b >> hFlush h >> return (return ())) (\ e -> return (failMisc (show e))) bufferReadLine :: HStream a => HandleStream a -> IO (Result a) bufferReadLine ref = onNonClosedDo ref $ \ conn -> do case connInput conn of Just c -> do let (a,b0) = buf_span (connBuffer conn) (/='\n') c let (newl,b1) = buf_splitAt (connBuffer conn) 1 b0 modifyMVar_ (getRef ref) (\ co -> return co{connInput=Just b1}) return (return (buf_append (connBuffer conn) a newl)) _ -> Prelude.catch (buf_hGetLine (connBuffer conn) (connHandle conn) >>= return . return . appendNL (connBuffer conn)) (\ e -> if isEOFError e then do when (connCloseEOF conn) $ catch (closeQuick ref) (\ _ -> return ()) return (return (buf_empty (connBuffer conn))) else return (failMisc (show e))) where -- yes, this s**ks.. _may_ have to be addressed if perf -- suggests worthiness. appendNL ops b = buf_snoc ops b nl nl :: Word8 nl = fromIntegral (fromEnum '\n') onNonClosedDo :: HandleStream a -> (Conn a -> IO (Result b)) -> IO (Result b) onNonClosedDo h act = do x <- readMVar (getRef h) case x of ConnClosed{} -> return (failWith ErrorClosed) _ -> act x