Network/TCP.hs
{-# LANGUAGE TypeSynonymInstances #-}
-----------------------------------------------------------------------------
-- |
-- Module : Network.TCP
-- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004
-- License : BSD
--
-- Maintainer : bjorn@bringert.net
-- Stability : experimental
-- Portability : non-portable (not tested)
--
-- An easy access TCP library. Makes the use of TCP in Haskell much easier.
-- This was originally part of Gray's\/Bringert's HTTP module.
--
-- * Changes by Robin Bate Boerop <robin@bateboerop.name>:
-- - Made dependencies explicit in import statements.
-- - Removed false dependencies from import statements.
-- - Removed unused exported functions.
--
-- * Changes by Simon Foster:
-- - Split module up into to separate Network.[Stream,TCP,HTTP] modules
--
-----------------------------------------------------------------------------
module Network.TCP
( Connection
, openTCPPort
, isConnectedTo
, openTCPConnection
, isTCPConnectedTo
, HandleStream
, HStream(..)
, StreamHooks(..)
, nullHooks
, setStreamHooks
) where
import Network.BSD (getHostByName, hostAddresses)
import Network.Socket
( Socket, SockAddr(SockAddrInet), SocketOption(KeepAlive)
, SocketType(Stream), inet_addr, connect
, shutdown, ShutdownCmd(ShutdownSend, ShutdownReceive)
, sClose, sIsConnected, setSocketOption
, socket, Family(AF_INET)
)
import qualified Network.Stream as Stream
( Stream(readBlock, readLine, writeBlock, close) )
import Network.Stream
( ConnError(..)
, Result
, failWith
)
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 )
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 Conn a
= MkConn { connSock :: ! Socket
, connHandle :: Handle
, connBuffer :: BufferOp a
, connInput :: Maybe a
, connHost :: String
, connHooks :: Maybe (StreamHooks a)
}
| ConnClosed
deriving(Eq)
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 ()
}
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 ()
}
setStreamHooks :: HandleStream ty -> StreamHooks ty -> IO ()
setStreamHooks h sh = modifyMVar_ (getRef h) (\ c -> return c{connHooks=Just sh})
class BufferType bufType => HStream bufType where
openStream :: String -> Int -> 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 ()
instance HStream Strict.ByteString where
openStream = openTCPConnection
readBlock c n = readBlockBS c n
readLine c = readLineBS c
writeBlock c str = writeBlockBS c str
close c = closeIt c Strict.null
instance HStream Lazy.ByteString where
openStream = \ a b -> openTCPConnection_ a b True
readBlock c n = readBlockBS c n
readLine c = readLineBS c
writeBlock c str = writeBlockBS c str
close c = closeIt c Lazy.null
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
instance HStream String where
openStream = openTCPConnection
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
-- | This function establishes a connection to a remote
-- host, it uses "getHostByName" which interrogates 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 =
do { s <- socket AF_INET Stream 6
; setSocketOption s KeepAlive 1
; host <- catchIO (inet_addr uri) -- handles ascii IP numbers
(\ _ -> getHostByName uri >>= \host ->
case hostAddresses host of
[] -> return (error "no addresses in host entry")
(h:_) -> return h)
; let a = SockAddrInet (toEnum port) host
; catchIO (connect s a) (\e -> sClose s >> ioError e)
; h <- socketToHandle s ReadWriteMode
; mb <- case stashInput of { True -> liftM Just $ buf_hGetContents bufferOps h; _ -> return Nothing }
; let conn =
(MkConn { connSock = s
, connHandle = h
, connBuffer = bufferOps
, connInput = mb
, connHost = uri
, connHooks = Nothing
})
; v <- newMVar conn
; return (HandleStream v)
}
closeConnection :: 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
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 -> String -> IO Bool
isConnectedTo (Connection conn) name = do
v <- readMVar (getRef conn)
case v of
ConnClosed -> return False
_
| map toLower (connHost v) == map toLower name -> sIsConnected (connSock v)
| otherwise -> return False
isTCPConnectedTo :: HandleStream ty -> String -> IO Bool
isTCPConnectedTo conn name = do
v <- readMVar (getRef conn)
case v of
ConnClosed -> return False
_
| map toLower (connHost v) == map toLower name -> sIsConnected (connSock v)
| otherwise -> return False
readBlockBS :: 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 to the user to decypher)
readLineBS :: 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 :: HandleStream ty -> (ty -> Bool) -> IO ()
closeIt c p = do
closeConnection c (readLineBS c >>= \ x -> case x of { Right xs -> return (p xs); _ -> return True})
conn <- readMVar (getRef c)
maybe (return ())
(hook_close)
(connHooks' conn)
bufferGetBlock :: 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 return (return (buf_empty (connBuffer conn)))
else return (fail (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 (fail (show e)))
bufferReadLine :: HandleStream a -> IO (Result a) -- BufferOp a -> Handle -> 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 return (return (buf_empty (connBuffer conn)))
else return (fail (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)
c -> act c