network-transport-tcp 0.5.1 → 0.6.0
raw patch · 5 files changed
+1187/−443 lines, 5 filesdep ~bytestringdep ~network-transportPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: bytestring, network-transport
API changes (from Hackage documentation)
- Network.Transport.TCP: decodeEndPointAddress :: EndPointAddress -> Maybe (HostName, ServiceName, EndPointId)
- Network.Transport.TCP: encodeEndPointAddress :: HostName -> ServiceName -> EndPointId -> EndPointAddress
- Network.Transport.TCP: instance GHC.Enum.Bounded Network.Transport.TCP.ConnectionRequestResponse
- Network.Transport.TCP: instance GHC.Enum.Bounded Network.Transport.TCP.ControlHeader
- Network.Transport.TCP: instance GHC.Enum.Enum Network.Transport.TCP.ConnectionRequestResponse
- Network.Transport.TCP: instance GHC.Enum.Enum Network.Transport.TCP.ControlHeader
- Network.Transport.TCP: instance GHC.Show.Show Network.Transport.TCP.ConnectionRequestResponse
- Network.Transport.TCP: instance GHC.Show.Show Network.Transport.TCP.ControlHeader
- Network.Transport.TCP.Internal: recvInt32 :: Num a => Socket -> IO a
+ Network.Transport.TCP: CloseEndPoint :: ControlHeader
+ Network.Transport.TCP: ConnectionRequestHostMismatch :: ConnectionRequestResponse
+ Network.Transport.TCP: ConnectionRequestUnsupportedVersion :: ConnectionRequestResponse
+ Network.Transport.TCP: QDisc :: IO t -> (EndPointAddress -> Event -> t -> IO ()) -> QDisc t
+ Network.Transport.TCP: [newEndPointInternal] :: TransportInternals -> (forall t. Maybe (QDisc t)) -> IO (Either (TransportError NewEndPointErrorCode) EndPoint)
+ Network.Transport.TCP: [qdiscDequeue] :: QDisc t -> IO t
+ Network.Transport.TCP: [qdiscEnqueue] :: QDisc t -> EndPointAddress -> Event -> t -> IO ()
+ Network.Transport.TCP: [tcpCheckPeerHost] :: TCPParameters -> Bool
+ Network.Transport.TCP: [tcpMaxAddressLength] :: TCPParameters -> Word32
+ Network.Transport.TCP: [tcpMaxReceiveLength] :: TCPParameters -> Word32
+ Network.Transport.TCP: [tcpNewQDisc] :: TCPParameters -> forall t. IO (QDisc t)
+ Network.Transport.TCP: data QDisc t
+ Network.Transport.TCP: simpleOnePlaceQDisc :: forall t. IO (QDisc t)
+ Network.Transport.TCP: simpleUnboundedQDisc :: forall t. IO (QDisc t)
+ Network.Transport.TCP.Internal: CloseConnection :: ControlHeader
+ Network.Transport.TCP.Internal: CloseEndPoint :: ControlHeader
+ Network.Transport.TCP.Internal: CloseSocket :: ControlHeader
+ Network.Transport.TCP.Internal: ConnectionRequestAccepted :: ConnectionRequestResponse
+ Network.Transport.TCP.Internal: ConnectionRequestCrossed :: ConnectionRequestResponse
+ Network.Transport.TCP.Internal: ConnectionRequestHostMismatch :: ConnectionRequestResponse
+ Network.Transport.TCP.Internal: ConnectionRequestInvalid :: ConnectionRequestResponse
+ Network.Transport.TCP.Internal: ConnectionRequestUnsupportedVersion :: ConnectionRequestResponse
+ Network.Transport.TCP.Internal: CreatedNewConnection :: ControlHeader
+ Network.Transport.TCP.Internal: ProbeSocket :: ControlHeader
+ Network.Transport.TCP.Internal: ProbeSocketAck :: ControlHeader
+ Network.Transport.TCP.Internal: currentProtocolVersion :: ProtocolVersion
+ Network.Transport.TCP.Internal: data ConnectionRequestResponse
+ Network.Transport.TCP.Internal: data ControlHeader
+ Network.Transport.TCP.Internal: decodeConnectionRequestResponse :: Word32 -> Maybe ConnectionRequestResponse
+ Network.Transport.TCP.Internal: decodeControlHeader :: Word32 -> Maybe ControlHeader
+ Network.Transport.TCP.Internal: decodeEndPointAddress :: EndPointAddress -> Maybe (HostName, ServiceName, EndPointId)
+ Network.Transport.TCP.Internal: decodeSockAddr :: SockAddr -> IO (Maybe (HostName, ServiceName))
+ Network.Transport.TCP.Internal: encodeConnectionRequestResponse :: ConnectionRequestResponse -> Word32
+ Network.Transport.TCP.Internal: encodeControlHeader :: ControlHeader -> Word32
+ Network.Transport.TCP.Internal: encodeEndPointAddress :: HostName -> ServiceName -> EndPointId -> EndPointAddress
+ Network.Transport.TCP.Internal: encodeWord32 :: Word32 -> ByteString
+ Network.Transport.TCP.Internal: instance GHC.Show.Show Network.Transport.TCP.Internal.ConnectionRequestResponse
+ Network.Transport.TCP.Internal: instance GHC.Show.Show Network.Transport.TCP.Internal.ControlHeader
+ Network.Transport.TCP.Internal: recvWord32 :: Socket -> IO Word32
+ Network.Transport.TCP.Internal: tryShutdownSocketBoth :: Socket -> IO ()
+ Network.Transport.TCP.Internal: type EndPointId = Word32
+ Network.Transport.TCP.Internal: type ProtocolVersion = Word32
- Network.Transport.TCP: TCPParameters :: Int -> Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> TCPParameters
+ Network.Transport.TCP: TCPParameters :: Int -> Bool -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> (forall t. IO (QDisc t)) -> Word32 -> Word32 -> Bool -> TCPParameters
- Network.Transport.TCP: TransportInternals :: ThreadId -> (EndPointAddress -> EndPointAddress -> IO Socket) -> TransportInternals
+ Network.Transport.TCP: TransportInternals :: ThreadId -> ((forall t. Maybe (QDisc t)) -> IO (Either (TransportError NewEndPointErrorCode) EndPoint)) -> (EndPointAddress -> EndPointAddress -> IO Socket) -> TransportInternals
- Network.Transport.TCP: createTransport :: HostName -> ServiceName -> TCPParameters -> IO (Either IOException Transport)
+ Network.Transport.TCP: createTransport :: HostName -> ServiceName -> (ServiceName -> (HostName, ServiceName)) -> TCPParameters -> IO (Either IOException Transport)
- Network.Transport.TCP: createTransportExposeInternals :: HostName -> ServiceName -> TCPParameters -> IO (Either IOException (Transport, TransportInternals))
+ Network.Transport.TCP: createTransportExposeInternals :: HostName -> ServiceName -> (ServiceName -> (HostName, ServiceName)) -> TCPParameters -> IO (Either IOException (Transport, TransportInternals))
- Network.Transport.TCP: socketToEndPoint :: EndPointAddress -> EndPointAddress -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> IO (Either (TransportError ConnectErrorCode) (Socket, ConnectionRequestResponse))
+ Network.Transport.TCP: socketToEndPoint :: EndPointAddress -> EndPointAddress -> Bool -> Bool -> Bool -> Maybe Int -> Maybe Int -> IO (Either (TransportError ConnectErrorCode) (MVar (), Socket, ConnectionRequestResponse))
- Network.Transport.TCP.Internal: forkServer :: HostName -> ServiceName -> Int -> Bool -> (SomeException -> IO ()) -> (Socket -> IO ()) -> IO (ServiceName, ThreadId)
+ Network.Transport.TCP.Internal: forkServer :: HostName -> ServiceName -> Int -> Bool -> (SomeException -> IO ()) -> (SomeException -> IO ()) -> (IO () -> (Socket, SockAddr) -> IO ()) -> IO (ServiceName, ThreadId)
- Network.Transport.TCP.Internal: recvExact :: Socket -> Int32 -> IO [ByteString]
+ Network.Transport.TCP.Internal: recvExact :: Socket -> Word32 -> IO [ByteString]
- Network.Transport.TCP.Internal: recvWithLength :: Socket -> IO [ByteString]
+ Network.Transport.TCP.Internal: recvWithLength :: Word32 -> Socket -> IO [ByteString]
Files
- ChangeLog +14/−0
- network-transport-tcp.cabal +4/−3
- src/Network/Transport/TCP.hs +547/−280
- src/Network/Transport/TCP/Internal.hs +254/−43
- tests/TestTCP.hs +368/−117
ChangeLog view
@@ -1,3 +1,17 @@+2017-08-21 FacundoDominguez <facundo.dominguez@tweag.io> 0.6.0++* Implemented protocol versioning (#55)+* Extend interface so queue policies and lengths can be configured.+* Check peer-reported host against socket host (#54)+* Test improvements+* Fix races when an EndPoint is closed or failed (#60)+* Fix timeout socket connections (#53)+* Use equality rather than ordering in socket close (#56)+* apiCloseEndPoint blocks until no longer reciving+* Shutdown sockets when closing endpoints+* Allow computing the external address from the chosen bind port (#50)+* Discard remote endpoints when they close or fail (#43)+ 2016-02-17 FacundoDominguez <facundo.dominguez@tweag.io> 0.5.0 * Add TCP_KEEPALIVE support for sockets.
network-transport-tcp.cabal view
@@ -1,5 +1,5 @@ Name: network-transport-tcp-Version: 0.5.1+Version: 0.6.0 Cabal-Version: >=1.10 Build-Type: Simple License: BSD3@@ -26,7 +26,7 @@ Library Build-Depends: base >= 4.3 && < 5,- network-transport >= 0.4.1.0 && < 0.5,+ network-transport >= 0.5 && < 0.6, data-accessor >= 0.2 && < 0.3, containers >= 0.4 && < 0.6, bytestring >= 0.9 && < 0.11,@@ -47,9 +47,10 @@ Type: exitcode-stdio-1.0 Main-Is: TestTCP.hs Build-Depends: base >= 4.3 && < 5,+ bytestring >= 0.9 && < 0.11, network-transport-tests >= 0.2.1.0 && < 0.3, network >= 2.3 && < 2.7,- network-transport >= 0.4.1.0 && < 0.5,+ network-transport, network-transport-tcp ghc-options: -threaded -rtsopts -with-rtsopts=-N HS-Source-Dirs: tests
src/Network/Transport/TCP.hs view
@@ -13,6 +13,7 @@ {-# LANGUAGE RecursiveDo #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE RankNTypes #-} module Network.Transport.TCP ( -- * Main API@@ -23,14 +24,15 @@ , createTransportExposeInternals , TransportInternals(..) , EndPointId- , encodeEndPointAddress- , decodeEndPointAddress , ControlHeader(..) , ConnectionRequestResponse(..) , firstNonReservedLightweightConnectionId , firstNonReservedHeavyweightConnectionId , socketToEndPoint , LightweightConnectionId+ , QDisc(..)+ , simpleUnboundedQDisc+ , simpleOnePlaceQDisc -- * Design notes -- $design ) where@@ -44,14 +46,27 @@ import Network.Transport import Network.Transport.TCP.Internal- ( forkServer+ ( ControlHeader(..)+ , encodeControlHeader+ , decodeControlHeader+ , ConnectionRequestResponse(..)+ , encodeConnectionRequestResponse+ , decodeConnectionRequestResponse+ , forkServer , recvWithLength- , recvInt32+ , recvExact+ , recvWord32+ , encodeWord32 , tryCloseSocket+ , tryShutdownSocketBoth+ , decodeSockAddr+ , EndPointId+ , encodeEndPointAddress+ , decodeEndPointAddress+ , currentProtocolVersion ) import Network.Transport.Internal- ( encodeInt32- , prependLength+ ( prependLength , mapIOException , tryIO , tryToEnum@@ -80,6 +95,7 @@ , connect , sOMAXCONN , AddrInfo+ , SockAddr(..) ) #ifdef USE_MOCK_NETWORK@@ -103,6 +119,7 @@ , modifyMVar , modifyMVar_ , readMVar+ , takeMVar , putMVar , newEmptyMVar , withMVar@@ -150,7 +167,7 @@ import qualified Data.Accessor.Container as DAC (mapMaybe) import Data.Foldable (forM_, mapM_) import qualified System.Timeout (timeout)-+import qualified Data.ByteString as BS (length) -- $design --@@ -264,10 +281,12 @@ -- ValidRemoteEndPointState). data TCPTransport = TCPTransport- { transportHost :: !N.HostName- , transportPort :: !N.ServiceName- , transportState :: !(MVar TransportState)- , transportParams :: !TCPParameters+ { transportHost :: !N.HostName+ , transportPort :: !N.ServiceName+ , transportBindHost :: !N.HostName+ , transportBindPort :: !N.ServiceName+ , transportState :: !(MVar TransportState)+ , transportParams :: !TCPParameters } data TransportState =@@ -281,8 +300,10 @@ data LocalEndPoint = LocalEndPoint { localAddress :: !EndPointAddress- , localChannel :: !(Chan Event) , localState :: !(MVar LocalEndPointState)+ -- | A 'QDisc' is held here rather than on the 'ValidLocalEndPointState'+ -- because even closed 'LocalEndPoint's can have queued input data.+ , localQueue :: !(QDisc Event) } data LocalEndPointState =@@ -414,18 +435,20 @@ data ValidRemoteEndPointState = ValidRemoteEndPointState { _remoteOutgoing :: !Int , _remoteIncoming :: !(Set LightweightConnectionId)- , _remoteMaxIncoming :: !LightweightConnectionId+ , _remoteLastIncoming :: !LightweightConnectionId , _remoteNextConnOutId :: !LightweightConnectionId , remoteSocket :: !N.Socket -- | When the connection is being probed, yields an IO action that can be -- used to release any resources dedicated to the probing. , remoteProbing :: Maybe (IO ()) , remoteSendLock :: !(MVar ())+ -- | An IO which returns when the socket (remoteSocket) has been closed.+ -- The program/thread which created the socket is always responsible+ -- for closing it, but sometimes other threads need to know when this+ -- happens.+ , remoteSocketClosed :: !(IO ()) } --- | Local identifier for an endpoint within this transport-type EndPointId = Word32- -- | Pair of local and a remote endpoint (for conciseness in signatures) type EndPointPair = (LocalEndPoint, RemoteEndPoint) @@ -441,30 +464,6 @@ -- 'LightweightConnectionId'. type HeavyweightConnectionId = Word32 --- | Control headers-data ControlHeader =- -- | Tell the remote endpoint that we created a new connection- CreatedNewConnection- -- | Tell the remote endpoint we will no longer be using a connection- | CloseConnection- -- | Request to close the connection (see module description)- | CloseSocket- -- | Message sent to probe a socket- | ProbeSocket- -- | Acknowledgement of the ProbeSocket message- | ProbeSocketAck- deriving (Enum, Bounded, Show)---- | Response sent by /B/ to /A/ when /A/ tries to connect-data ConnectionRequestResponse =- -- | /B/ accepts the connection- ConnectionRequestAccepted- -- | /A/ requested an invalid endpoint- | ConnectionRequestInvalid- -- | /A/s request crossed with a request from /B/ (see protocols)- | ConnectionRequestCrossed- deriving (Enum, Bounded, Show)- -- | Parameters for setting up the TCP transport data TCPParameters = TCPParameters { -- | Backlog for 'listen'.@@ -479,26 +478,51 @@ -- | Should we set TCP_NODELAY on connection sockets? -- Defaults to True. , tcpNoDelay :: Bool- -- | Value of TCP_USER_TIMEOUT in milliseconds- , tcpKeepAlive :: Bool -- | Should we set TCP_KEEPALIVE on connection sockets?+ , tcpKeepAlive :: Bool+ -- | Value of TCP_USER_TIMEOUT in milliseconds , tcpUserTimeout :: Maybe Int -- | A connect timeout for all 'connect' calls of the transport -- in microseconds -- -- This can be overriden for each connect call with -- 'ConnectHints'.'connectTimeout'.+ --+ -- Connection requests to this transport will also timeout if they don't+ -- send the required data before this many microseconds. , transportConnectTimeout :: Maybe Int+ -- | Create a QDisc for an EndPoint.+ , tcpNewQDisc :: forall t . IO (QDisc t)+ -- | Maximum length (in bytes) for a peer's address.+ -- If a peer attempts to send an address of length exceeding the limit,+ -- the connection will be refused (socket will close).+ , tcpMaxAddressLength :: Word32+ -- | Maximum length (in bytes) to receive from a peer.+ -- If a peer attempts to send data on a lightweight connection exceeding+ -- the limit, the heavyweight connection which carries that lightweight+ -- connection will go down. The peer and the local node will get an+ -- EventConnectionLost.+ , tcpMaxReceiveLength :: Word32+ -- | If True, new connections will be accepted only if the socket's host+ -- matches the host that the peer claims in its EndPointAddress.+ -- This is useful when operating on untrusted networks, because the peer+ -- could otherwise deny service to some victim by claiming the victim's+ -- address.+ , tcpCheckPeerHost :: Bool } -- | Internal functionality we expose for unit testing data TransportInternals = TransportInternals { -- | The ID of the thread that listens for new incoming connections- transportThread :: ThreadId+ transportThread :: ThreadId+ -- | A variant of newEndPoint in which the QDisc determined by the+ -- transport's TCPParameters can be optionally overridden.+ , newEndPointInternal :: (forall t . Maybe (QDisc t))+ -> IO (Either (TransportError NewEndPointErrorCode) EndPoint) -- | Find the socket between a local and a remote endpoint- , socketBetween :: EndPointAddress- -> EndPointAddress- -> IO N.Socket+ , socketBetween :: EndPointAddress+ -> EndPointAddress+ -> IO N.Socket } --------------------------------------------------------------------------------@@ -506,20 +530,25 @@ -------------------------------------------------------------------------------- -- | Create a TCP transport-createTransport :: N.HostName- -> N.ServiceName+createTransport :: N.HostName -- ^ Bind host name.+ -> N.ServiceName -- ^ Bind port.+ -> (N.ServiceName -> (N.HostName, N.ServiceName))+ -- ^ External address host name and port, computed from the+ -- actual bind port. -> TCPParameters -> IO (Either IOException Transport)-createTransport host port params =- either Left (Right . fst) <$> createTransportExposeInternals host port params+createTransport bindHost bindPort mkExternal params =+ either Left (Right . fst) <$>+ createTransportExposeInternals bindHost bindPort mkExternal params -- | You should probably not use this function (used for unit testing only) createTransportExposeInternals :: N.HostName -> N.ServiceName+ -> (N.ServiceName -> (N.HostName, N.ServiceName)) -> TCPParameters -> IO (Either IOException (Transport, TransportInternals))-createTransportExposeInternals host port params = do+createTransportExposeInternals bindHost bindPort mkExternal params = do state <- newMVar . TransportValid $ ValidTransportState { _localEndPoints = Map.empty , _nextEndPointId = 0@@ -535,16 +564,20 @@ -- completes (see description of 'forkServer'), yet we need the port to -- construct a transport. So we tie a recursive knot. (port', result) <- do- let transport = TCPTransport { transportState = state- , transportHost = host- , transportPort = port'- , transportParams = params+ let (externalHost, externalPort) = mkExternal port'+ let transport = TCPTransport { transportState = state+ , transportHost = externalHost+ , transportPort = externalPort+ , transportBindHost = bindHost+ , transportBindPort = port'+ , transportParams = params } bracketOnError (forkServer- host- port+ bindHost+ bindPort (tcpBacklog params) (tcpReuseServerAddr params)+ (errorHandler transport) (terminationHandler transport) (handleConnectionRequest transport)) (\(_port', tid) -> killThread tid)@@ -554,20 +587,29 @@ mkTransport :: TCPTransport -> ThreadId -> IO (Transport, TransportInternals)- mkTransport transport tid = return- ( Transport- { newEndPoint = apiNewEndPoint transport- , closeTransport = let evs = [ EndPointClosed- , throw $ userError "Transport closed"- ] in- apiCloseTransport transport (Just tid) evs- }- , TransportInternals- { transportThread = tid- , socketBetween = internalSocketBetween transport- }- )+ mkTransport transport tid = do+ return+ ( Transport+ { newEndPoint = do+ qdisc <- tcpNewQDisc params+ apiNewEndPoint transport qdisc+ , closeTransport = let evs = [ EndPointClosed ]+ in apiCloseTransport transport (Just tid) evs+ }+ , TransportInternals+ { transportThread = tid+ , socketBetween = internalSocketBetween transport+ , newEndPointInternal = \mqdisc -> case mqdisc of+ Just qdisc -> apiNewEndPoint transport qdisc+ Nothing -> do+ qdisc <- tcpNewQDisc params+ apiNewEndPoint transport qdisc+ }+ ) + errorHandler :: TCPTransport -> SomeException -> IO ()+ errorHandler _ = throwIO+ terminationHandler :: TCPTransport -> SomeException -> IO () terminationHandler transport ex = do let evs = [ ErrorEvent (TransportError EventTransportFailed (show ex))@@ -584,7 +626,11 @@ , tcpNoDelay = False , tcpKeepAlive = False , tcpUserTimeout = Nothing+ , tcpNewQDisc = simpleUnboundedQDisc , transportConnectTimeout = Nothing+ , tcpMaxAddressLength = maxBound+ , tcpMaxReceiveLength = maxBound+ , tcpCheckPeerHost = False } --------------------------------------------------------------------------------@@ -606,18 +652,17 @@ -- | Create a new endpoint apiNewEndPoint :: TCPTransport+ -> QDisc Event -> IO (Either (TransportError NewEndPointErrorCode) EndPoint)-apiNewEndPoint transport =+apiNewEndPoint transport qdisc = try . asyncWhenCancelled closeEndPoint $ do- ourEndPoint <- createLocalEndPoint transport+ ourEndPoint <- createLocalEndPoint transport qdisc return EndPoint- { receive = readChan (localChannel ourEndPoint)+ { receive = qdiscDequeue (localQueue ourEndPoint) , address = localAddress ourEndPoint , connect = apiConnect (transportParams transport) ourEndPoint- , closeEndPoint = let evs = [ EndPointClosed- , throw $ userError "Endpoint closed"- ] in- apiCloseEndPoint transport evs ourEndPoint+ , closeEndPoint = let evs = [ EndPointClosed ]+ in apiCloseEndPoint transport evs ourEndPoint , newMulticastGroup = return . Left $ newMulticastGroupError , resolveMulticastGroup = return . Left . const resolveMulticastGroupError }@@ -627,6 +672,75 @@ resolveMulticastGroupError = TransportError ResolveMulticastGroupUnsupported "Multicast not supported" +-- | Abstraction of a queue for an 'EndPoint'.+--+-- A value of type @QDisc t@ is a queue of events of an abstract type @t@.+--+-- This specifies which 'Event's will come from+-- 'receive :: EndPoint -> IO Event' and when. It is highly general so that+-- the simple yet potentially very fast implementation backed by a single+-- unbounded channel can be used, without excluding more nuanced policies+-- like class-based queueing with bounded buffers for each peer, which may be+-- faster in certain conditions but probably has lower maximal throughput.+--+-- A 'QDisc' must satisfy some properties in order for the semantics of+-- network-transport to hold true. In general, an event fed with+-- 'qdiscEnqueue' must not be dropped. i.e. provided that no other event in+-- the QDisc has higher priority, the event should eventually be returned by+-- 'qdiscDequeue'. An exception to this are 'Receive' events of unreliable+-- connections.+--+-- Every call to 'receive' is just 'qdiscDequeue' on that 'EndPoint's+-- 'QDisc'. Whenever an event arises from a socket, `qdiscEnqueue` is called+-- with the relevant metadata in the same thread that reads from the socket.+-- You can be clever about when to block here, so as to control network+-- ingress. This applies also to loopback connections (an 'EndPoint' connects+-- to itself), in which case blocking on the enqueue would only block some+-- thread in your program rather than some chatty network peer. The 'Event'+-- which is to be enqueued is given to 'qdiscEnqueue' so that the 'QDisc'+-- can know about open connections, their identifiers and peer addresses, etc.+data QDisc t = QDisc {+ -- | Dequeue an event.+ qdiscDequeue :: IO t+ -- | @qdiscEnqueue ep ev t@ enqueues and event @t@, originated from the+ -- given remote endpoint @ep@ and with data @ev@.+ --+ -- @ep@ might be the local endpoint if it relates to a self-connection.+ --+ -- @ev@ might be in practice the value given as @t@. It is passed in+ -- the abstract form @t@ to enforce it is dequeued unmodified, but the+ -- 'QDisc' implementation can still observe the concrete form @ev@ to+ -- make prioritization decisions.+ , qdiscEnqueue :: EndPointAddress -> Event -> t -> IO ()+ }++-- | Post an 'Event' using a 'QDisc'.+qdiscEnqueue' :: QDisc Event -> EndPointAddress -> Event -> IO ()+qdiscEnqueue' qdisc addr event = qdiscEnqueue qdisc addr event event++-- | A very simple QDisc backed by an unbounded channel.+simpleUnboundedQDisc :: forall t . IO (QDisc t)+simpleUnboundedQDisc = do+ eventChan <- newChan+ return $ QDisc {+ qdiscDequeue = readChan eventChan+ , qdiscEnqueue = const (const (writeChan eventChan))+ }++-- | A very simple QDisc backed by a 1-place queue (MVar).+-- With this QDisc, all threads reading from sockets will try to put their+-- events into the same MVar. That MVar will be cleared by calls to+-- 'receive'. Thus the rate at which data is read from the wire is directly+-- related to the rate at which data is pulled from the EndPoint by+-- 'receive'.+simpleOnePlaceQDisc :: forall t . IO (QDisc t)+simpleOnePlaceQDisc = do+ mvar <- newEmptyMVar+ return $ QDisc {+ qdiscDequeue = takeMVar mvar+ , qdiscEnqueue = const (const (putMVar mvar))+ }+ -- | Connnect to an endpoint apiConnect :: TCPParameters -- ^ Parameters -> LocalEndPoint -- ^ Local end point@@ -662,7 +776,10 @@ then do writeIORef connAlive False sched theirEndPoint $- sendOn vst [encodeInt32 CloseConnection, encodeInt32 connId]+ sendOn vst [+ encodeWord32 (encodeControlHeader CloseConnection)+ , encodeWord32 connId+ ] return ( RemoteEndPointValid . (remoteOutgoing ^: (\x -> x - 1)) $ vst@@ -692,7 +809,7 @@ alive <- readIORef connAlive if alive then sched theirEndPoint $- sendOn vst (encodeInt32 connId : prependLength payload)+ sendOn vst (encodeWord32 connId : prependLength payload) else throwIO $ TransportError SendClosed "Connection closed" RemoteEndPointClosing _ _ -> do alive <- readIORef connAlive@@ -730,7 +847,8 @@ return (LocalEndPointClosed, Nothing) forM_ mOurState $ \vst -> do forM_ (vst ^. localConnections) tryCloseRemoteSocket- forM_ evs $ writeChan (localChannel ourEndPoint)+ let qdisc = localQueue ourEndPoint+ forM_ evs (qdiscEnqueue' qdisc (localAddress ourEndPoint)) where -- Close the remote socket and return the set of all incoming connections tryCloseRemoteSocket :: RemoteEndPoint -> IO ()@@ -746,19 +864,32 @@ putMVar resolved () return closed RemoteEndPointValid vst -> do+ -- Schedule an action to send a CloseEndPoint message and then+ -- wait for the socket to actually close (meaning that this+ -- end point is no longer receiving from it).+ -- Since we replace the state in this MVar with 'closed', it's+ -- guaranteed that no other actions will be scheduled after this+ -- one. sched theirEndPoint $ do- void $ tryIO $ sendOn vst [ encodeInt32 CloseSocket- , encodeInt32 (vst ^. remoteMaxIncoming)- ]+ void $ tryIO $ sendOn vst+ [ encodeWord32 (encodeControlHeader CloseEndPoint) ] -- Release probing resources if probing. forM_ (remoteProbing vst) id- tryCloseSocket (remoteSocket vst)+ tryShutdownSocketBoth (remoteSocket vst)+ remoteSocketClosed vst return closed RemoteEndPointClosing resolved vst -> do -- Release probing resources if probing. forM_ (remoteProbing vst) id putMVar resolved ()- sched theirEndPoint $ tryCloseSocket (remoteSocket vst)+ -- Schedule an action to wait for the socket to actually close (this+ -- end point is no longer receiving from it).+ -- Since we replace the state in this MVar with 'closed', it's+ -- guaranteed that no other actions will be scheduled after this+ -- one.+ sched theirEndPoint $ do+ tryShutdownSocketBoth (remoteSocket vst)+ remoteSocketClosed vst return closed RemoteEndPointClosed -> return st@@ -781,128 +912,197 @@ -- the transport down. We must be careful to close the socket when a (possibly -- asynchronous, ThreadKilled) exception occurs. (If an exception escapes from -- handleConnectionRequest the transport will be shut down.)-handleConnectionRequest :: TCPTransport -> N.Socket -> IO ()-handleConnectionRequest transport sock = handle handleException $ do+handleConnectionRequest :: TCPTransport -> IO () -> (N.Socket, N.SockAddr) -> IO ()+handleConnectionRequest transport socketClosed (sock, sockAddr) = handle handleException $ do when (tcpNoDelay $ transportParams transport) $ N.setSocketOption sock N.NoDelay 1 when (tcpKeepAlive $ transportParams transport) $ N.setSocketOption sock N.KeepAlive 1 forM_ (tcpUserTimeout $ transportParams transport) $ N.setSocketOption sock N.UserTimeout- ourEndPointId <- recvInt32 sock- theirAddress <- EndPointAddress . BS.concat <$> recvWithLength sock- let ourAddress = encodeEndPointAddress (transportHost transport)- (transportPort transport)- ourEndPointId- ourEndPoint <- withMVar (transportState transport) $ \st -> case st of- TransportValid vst ->- case vst ^. localEndPointAt ourAddress of- Nothing -> do- sendMany sock [encodeInt32 ConnectionRequestInvalid]- throwIO $ userError "handleConnectionRequest: Invalid endpoint"- Just ourEndPoint ->- return ourEndPoint- TransportClosed ->- throwIO $ userError "Transport closed"- void . forkIO $ go ourEndPoint theirAddress+ let handleVersioned = do+ -- Always receive the protocol version and a handshake (content of the+ -- handshake is version-dependent, but the length is always sent,+ -- regardless of the version).+ protocolVersion <- recvWord32 sock+ handshakeLength <- recvWord32 sock+ -- For now we support only version 0.0.0.0.+ case protocolVersion of+ 0x00000000 -> handleConnectionRequestV0 (sock, sockAddr)+ _ -> do+ -- Inform the peer that we want version 0x00000000+ sendMany sock [+ encodeWord32 (encodeConnectionRequestResponse ConnectionRequestUnsupportedVersion)+ , encodeWord32 0x00000000+ ]+ -- Clear the socket of the unsupported handshake data.+ _ <- recvExact sock handshakeLength+ handleVersioned+ -- The handshake must complete within the optional timeout duration.+ -- No socket 'recv's are to be run outside the timeout. The continuation+ -- returned may 'send', but not 'recv'.+ let connTimeout = transportConnectTimeout (transportParams transport)+ outcome <- maybe (fmap Just) System.Timeout.timeout connTimeout handleVersioned+ case outcome of+ Nothing -> throwIO (userError "handleConnectionRequest: timed out")+ Just act -> forM_ act id+ where- go :: LocalEndPoint -> EndPointAddress -> IO ()- go ourEndPoint theirAddress = do- -- This runs in a thread that will never be killed- mEndPoint <- handle ((>> return Nothing) . handleException) $ do++ handleException :: SomeException -> IO ()+ handleException ex = do+ rethrowIfAsync (fromException ex)++ rethrowIfAsync :: Maybe AsyncException -> IO ()+ rethrowIfAsync = mapM_ throwIO++ handleConnectionRequestV0 :: (N.Socket, N.SockAddr) -> IO (Maybe (IO ()))+ handleConnectionRequestV0 (sock, sockAddr) = do+ -- Get the OS-determined host and port.+ (actualHost, actualPort) <-+ decodeSockAddr sockAddr >>=+ maybe (throwIO (userError "handleConnectionRequest: invalid socket address")) return+ (ourEndPointId, theirAddress) <- do+ ourEndPointId <- recvWord32 sock+ let maxAddressLength = tcpMaxAddressLength $ transportParams transport+ theirAddress <- EndPointAddress . BS.concat <$>+ recvWithLength maxAddressLength sock+ return (ourEndPointId, theirAddress)+ let ourAddress = encodeEndPointAddress (transportHost transport)+ (transportPort transport)+ ourEndPointId+ (theirHost, _, _)+ <- maybe (throwIO (userError "handleConnectionRequest: peer gave malformed address"))+ return+ (decodeEndPointAddress theirAddress)+ let checkPeerHost = tcpCheckPeerHost (transportParams transport)+ if checkPeerHost && (theirHost /= actualHost)+ then do+ -- If the OS-determined host doesn't match the host that the peer gave us,+ -- then we have no choice but to reject the connection. It's because we+ -- use the EndPointAddress to key the remote end points (localConnections)+ -- and we don't want to allow a peer to deny service to other peers by+ -- claiming to have their host and port.+ sendMany sock $+ encodeWord32 (encodeConnectionRequestResponse ConnectionRequestHostMismatch)+ : prependLength [BSC.pack actualHost]+ return Nothing+ else do+ ourEndPoint <- withMVar (transportState transport) $ \st -> case st of+ TransportValid vst ->+ case vst ^. localEndPointAt ourAddress of+ Nothing -> do+ sendMany sock [encodeWord32 (encodeConnectionRequestResponse ConnectionRequestInvalid)]+ throwIO $ userError "handleConnectionRequest: Invalid endpoint"+ Just ourEndPoint ->+ return ourEndPoint+ TransportClosed ->+ throwIO $ userError "Transport closed"+ return (Just (go ourEndPoint theirAddress))++ where++ go :: LocalEndPoint -> EndPointAddress -> IO ()+ go ourEndPoint theirAddress = handle handleException $ do resetIfBroken ourEndPoint theirAddress (theirEndPoint, isNew) <- findRemoteEndPoint ourEndPoint theirAddress RequestedByThem Nothing if not isNew then do- void $ tryIO $ sendMany sock [encodeInt32 ConnectionRequestCrossed]+ void $ tryIO $ sendMany sock+ [encodeWord32 (encodeConnectionRequestResponse ConnectionRequestCrossed)] probeIfValid theirEndPoint- tryCloseSocket sock- return Nothing else do sendLock <- newMVar () let vst = ValidRemoteEndPointState { remoteSocket = sock+ , remoteSocketClosed = socketClosed , remoteProbing = Nothing , remoteSendLock = sendLock , _remoteOutgoing = 0 , _remoteIncoming = Set.empty- , _remoteMaxIncoming = 0+ , _remoteLastIncoming = 0 , _remoteNextConnOutId = firstNonReservedLightweightConnectionId }- sendMany sock [encodeInt32 ConnectionRequestAccepted]+ sendMany sock [encodeWord32 (encodeConnectionRequestResponse ConnectionRequestAccepted)]+ -- resolveInit will update the shared state, and handleIncomingMessages+ -- will always ultimately clean up after it.+ -- Closing up the socket is also out of our hands. It will happen+ -- when handleIncomingMessages finishes. resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst)- return (Just theirEndPoint)- -- If we left the scope of the exception handler with a return value of- -- Nothing then the socket is already closed; otherwise, the socket has- -- been recorded as part of the remote endpoint. Either way, we no longer- -- have to worry about closing the socket on receiving an asynchronous- -- exception from this point forward.- forM_ mEndPoint $ handleIncomingMessages . (,) ourEndPoint-- handleException :: SomeException -> IO ()- handleException ex = do- tryCloseSocket sock- rethrowIfAsync (fromException ex)-- rethrowIfAsync :: Maybe AsyncException -> IO ()- rethrowIfAsync = mapM_ throwIO+ `finally`+ handleIncomingMessages (transportParams transport) (ourEndPoint, theirEndPoint) - probeIfValid :: RemoteEndPoint -> IO ()- probeIfValid theirEndPoint = modifyMVar_ (remoteState theirEndPoint) $- \st -> case st of- RemoteEndPointValid- vst@(ValidRemoteEndPointState { remoteProbing = Nothing }) -> do- tid <- forkIO $ do- -- send probe- let params = transportParams transport- void $ tryIO $ System.Timeout.timeout- (maybe (-1) id $ transportConnectTimeout params) $ do- sendMany (remoteSocket vst) [encodeInt32 ProbeSocket]- threadDelay maxBound- -- Discard the connection if this thread is not killed (i.e. the- -- probe ack does not arrive on time).- --- -- The thread handling incoming messages will detect the socket is- -- closed and will report the failure upwards.- tryCloseSocket (remoteSocket vst)- -- Waiting the probe ack and closing the socket is only needed in- -- platforms where TCP_USER_TIMEOUT is not available or when the- -- user does not set it. Otherwise the ack would be handled at the- -- TCP level and the the thread handling incoming messages would- -- get the error.+ probeIfValid :: RemoteEndPoint -> IO ()+ probeIfValid theirEndPoint = modifyMVar_ (remoteState theirEndPoint) $+ \st -> case st of+ RemoteEndPointValid+ vst@(ValidRemoteEndPointState { remoteProbing = Nothing }) -> do+ tid <- forkIO $ do+ -- send probe+ let params = transportParams transport+ void $ tryIO $ System.Timeout.timeout+ (maybe (-1) id $ transportConnectTimeout params) $ do+ sendMany (remoteSocket vst)+ [encodeWord32 (encodeControlHeader ProbeSocket)]+ threadDelay maxBound+ -- Discard the connection if this thread is not killed (i.e. the+ -- probe ack does not arrive on time).+ --+ -- The thread handling incoming messages will detect the socket is+ -- closed and will report the failure upwards.+ tryCloseSocket (remoteSocket vst)+ -- Waiting the probe ack and closing the socket is only needed in+ -- platforms where TCP_USER_TIMEOUT is not available or when the+ -- user does not set it. Otherwise the ack would be handled at the+ -- TCP level and the the thread handling incoming messages would+ -- get the error. - return $ RemoteEndPointValid- vst { remoteProbing = Just (killThread tid) }- _ -> return st+ return $ RemoteEndPointValid+ vst { remoteProbing = Just (killThread tid) }+ _ -> return st -- | Handle requests from a remote endpoint. -- -- Returns only if the remote party closes the socket or if an error occurs. -- This runs in a thread that will never be killed.-handleIncomingMessages :: EndPointPair -> IO ()-handleIncomingMessages (ourEndPoint, theirEndPoint) = do- mSock <- withMVar theirState $ \st ->- case st of- RemoteEndPointInvalid _ ->- relyViolation (ourEndPoint, theirEndPoint)- "handleIncomingMessages (invalid)"- RemoteEndPointInit _ _ _ ->- relyViolation (ourEndPoint, theirEndPoint)- "handleIncomingMessages (init)"- RemoteEndPointValid ep ->- return . Just $ remoteSocket ep- RemoteEndPointClosing _ ep ->- return . Just $ remoteSocket ep- RemoteEndPointClosed ->- return Nothing- RemoteEndPointFailed _ ->- return Nothing+handleIncomingMessages :: TCPParameters -> EndPointPair -> IO ()+handleIncomingMessages params (ourEndPoint, theirEndPoint) =+ bracket acquire release act - forM_ mSock $ \sock ->- tryIO (go sock) >>= either (prematureExit sock) return where++ -- Use shared remote endpoint state to get a socket, or an appropriate+ -- exception in case it's neither valid nor closing.+ acquire :: IO (Either IOError N.Socket)+ acquire = withMVar theirState $ \st -> case st of+ RemoteEndPointInvalid _ ->+ relyViolation (ourEndPoint, theirEndPoint)+ "handleIncomingMessages (invalid)"+ RemoteEndPointInit _ _ _ ->+ relyViolation (ourEndPoint, theirEndPoint)+ "handleIncomingMessages (init)"+ RemoteEndPointValid ep ->+ return . Right $ remoteSocket ep+ RemoteEndPointClosing _ ep ->+ return . Right $ remoteSocket ep+ RemoteEndPointClosed ->+ return . Left $ userError "handleIncomingMessages (already closed)"+ RemoteEndPointFailed _ ->+ return . Left $ userError "handleIncomingMessages (failed)"++ -- 'Right' is the normal case in which there still is a live socket to+ -- the remote endpoint, and so 'act' was run and installed its own+ -- exception handler.+ release :: Either IOError N.Socket -> IO ()+ release (Left err) = prematureExit err+ release (Right _) = return ()++ act :: Either IOError N.Socket -> IO ()+ act (Left _) = return ()+ act (Right sock) = go sock `catch` prematureExit+ -- Dispatch -- -- If a recv throws an exception this will be caught top-level and@@ -913,24 +1113,46 @@ -- exception thrown by 'recv'. go :: N.Socket -> IO () go sock = do- lcid <- recvInt32 sock :: IO LightweightConnectionId+ lcid <- recvWord32 sock :: IO LightweightConnectionId if lcid >= firstNonReservedLightweightConnectionId then do readMessage sock lcid go sock else- case tryToEnum (fromIntegral lcid) of+ case decodeControlHeader lcid of Just CreatedNewConnection -> do- recvInt32 sock >>= createdNewConnection+ recvWord32 sock >>= createdNewConnection go sock Just CloseConnection -> do- recvInt32 sock >>= closeConnection+ recvWord32 sock >>= closeConnection go sock Just CloseSocket -> do- didClose <- recvInt32 sock >>= closeSocket sock+ didClose <- recvWord32 sock >>= closeSocket sock unless didClose $ go sock+ Just CloseEndPoint -> do+ let closeRemoteEndPoint vst = do+ forM_ (remoteProbing vst) id+ -- close incoming connections+ forM_ (Set.elems $ vst ^. remoteIncoming) $+ qdiscEnqueue' ourQueue theirAddr . ConnectionClosed . connId+ -- report the endpoint as gone if we have any outgoing+ -- connections+ when (vst ^. remoteOutgoing > 0) $ do+ let code = EventConnectionLost (remoteAddress theirEndPoint)+ qdiscEnqueue' ourQueue theirAddr . ErrorEvent $+ TransportError code "The remote endpoint was closed."+ removeRemoteEndPoint (ourEndPoint, theirEndPoint)+ modifyMVar_ theirState $ \s -> case s of+ RemoteEndPointValid vst -> do+ closeRemoteEndPoint vst+ return RemoteEndPointClosed+ RemoteEndPointClosing resolved vst -> do+ closeRemoteEndPoint vst+ putMVar resolved ()+ return RemoteEndPointClosed+ _ -> return s Just ProbeSocket -> do- forkIO $ sendMany sock [encodeInt32 ProbeSocketAck]+ forkIO $ sendMany sock [encodeWord32 (encodeControlHeader ProbeSocketAck)] go sock Just ProbeSocketAck -> do stopProbing@@ -951,7 +1173,7 @@ "handleIncomingMessages:createNewConnection (init)" RemoteEndPointValid vst -> return ( (remoteIncoming ^: Set.insert lcid)- $ (remoteMaxIncoming ^= lcid)+ $ (remoteLastIncoming ^= lcid) vst ) RemoteEndPointClosing resolved vst -> do@@ -963,7 +1185,7 @@ -- RemoteEndPointValid putMVar resolved () return ( (remoteIncoming ^= Set.singleton lcid)- . (remoteMaxIncoming ^= lcid)+ . (remoteLastIncoming ^= lcid) $ vst ) RemoteEndPointFailed err ->@@ -972,7 +1194,7 @@ relyViolation (ourEndPoint, theirEndPoint) "createNewConnection (closed)" return (RemoteEndPointValid vst)- writeChan ourChannel (ConnectionOpened (connId lcid) ReliableOrdered theirAddr)+ qdiscEnqueue' ourQueue theirAddr (ConnectionOpened (connId lcid) ReliableOrdered theirAddr) -- Close a connection -- It is important that we verify that the connection is in fact open,@@ -1000,7 +1222,7 @@ throwIO err RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "closeConnection (closed)"- writeChan ourChannel (ConnectionClosed (connId lcid))+ qdiscEnqueue' ourQueue theirAddr (ConnectionClosed (connId lcid)) -- Close the socket (if we don't have any outgoing connections) closeSocket :: N.Socket -> LightweightConnectionId -> IO Bool@@ -1018,16 +1240,28 @@ -- remote endpoint to indicate that all its connections to us are -- now properly closed forM_ (Set.elems $ vst ^. remoteIncoming) $- writeChan ourChannel . ConnectionClosed . connId+ qdiscEnqueue' ourQueue theirAddr . ConnectionClosed . connId let vst' = remoteIncoming ^= Set.empty $ vst- -- If we still have outgoing connections then we ignore the- -- CloseSocket request (we sent a ConnectionCreated message to the- -- remote endpoint, but it did not receive it before sending the- -- CloseSocket request). Similarly, if lastReceivedId < lastSentId- -- then we sent a ConnectionCreated *AND* a ConnectionClosed- -- message to the remote endpoint, *both of which* it did not yet- -- receive before sending the CloseSocket request.- if vst' ^. remoteOutgoing > 0 || lastReceivedId < lastSentId vst+ -- The peer sends the connection id of the last connection which+ -- they accepted from us.+ --+ -- If it's not the same as the id of the last connection that we+ -- have made to them (assuming we haven't cycled through all+ -- identifiers so fast) then they hadn't seen the request before+ -- they tried to close the socket. In that case, we don't close the+ -- socket. They'll see our in-flight connection request and then+ -- abandon their attempt to close the socket.+ --+ -- It's possible that a local connection is coming up but has not+ -- yet sent CreatedNewConnection (see createConnectionTo), in+ -- which case remoteOutgoing is positive, but the sent and received+ -- ids do match. In this case we don't close the socket, because+ -- that connection will soon sent the message and bump the lastSentId.+ --+ -- Both disjuncts are needed: it's possible that remoteOutgoing is+ -- 0 and the ids do not match, in case we have created and closed+ -- a connection but the peer has not yet heard of it.+ if vst ^. remoteOutgoing > 0 || lastReceivedId /= lastSentId vst then return (RemoteEndPointValid vst', Nothing) else do@@ -1036,10 +1270,10 @@ removeRemoteEndPoint (ourEndPoint, theirEndPoint) -- Attempt to reply (but don't insist) act <- schedule theirEndPoint $ do- void $ tryIO $ sendOn vst' [ encodeInt32 CloseSocket- , encodeInt32 (vst ^. remoteMaxIncoming)- ]- tryCloseSocket sock+ void $ tryIO $ sendOn vst'+ [ encodeWord32 (encodeControlHeader CloseSocket)+ , encodeWord32 (vst ^. remoteLastIncoming)+ ] return (RemoteEndPointClosed, Just act) RemoteEndPointClosing resolved vst -> do -- Like above, we need to check if there is a ConnectionCreated@@ -1047,16 +1281,26 @@ -- received. However, since we are in 'closing' state, the only -- way this may happen is when we sent a ConnectionCreated, -- ConnectionClosed, and CloseSocket message, none of which have- -- yet been received. We leave the endpoint in closing state in- -- that case.- if lastReceivedId < lastSentId vst+ -- yet been received. It's sufficient to check that the peer has+ -- not seen the ConnectionCreated message. In case they have seen+ -- it (so that lastReceivedId == lastSendId vst) then they must+ -- have seen the other messages or else they would not have sent+ -- CloseSocket.+ -- We leave the endpoint in closing state in that case.+ if lastReceivedId /= lastSentId vst then do return (RemoteEndPointClosing resolved vst, Nothing) else do -- Release probing resources if probing.+ when (vst ^. remoteOutgoing > 0) $ do+ let code = EventConnectionLost (remoteAddress theirEndPoint)+ let msg = "socket closed prematurely by peer"+ qdiscEnqueue' ourQueue theirAddr . ErrorEvent $ TransportError code msg forM_ (remoteProbing vst) id removeRemoteEndPoint (ourEndPoint, theirEndPoint)- act <- schedule theirEndPoint $ tryCloseSocket sock+ -- Nothing to do, but we want to indicate that the socket+ -- really did close.+ act <- schedule theirEndPoint $ return () putMVar resolved () return (RemoteEndPointClosed, Just act) RemoteEndPointFailed err ->@@ -1075,7 +1319,8 @@ -- overhead readMessage :: N.Socket -> LightweightConnectionId -> IO () readMessage sock lcid =- recvWithLength sock >>= writeChan ourChannel . Received (connId lcid)+ recvWithLength recvLimit sock >>=+ qdiscEnqueue' ourQueue theirAddr . Received (connId lcid) -- Stop probing a connection as a result of receiving a probe ack. stopProbing :: IO ()@@ -1087,14 +1332,15 @@ _ -> return st -- Arguments- ourChannel = localChannel ourEndPoint+ ourQueue = localQueue ourEndPoint+ ourState = localState ourEndPoint theirState = remoteState theirEndPoint theirAddr = remoteAddress theirEndPoint+ recvLimit = tcpMaxReceiveLength params -- Deal with a premature exit- prematureExit :: N.Socket -> IOException -> IO ()- prematureExit sock err = do- tryCloseSocket sock+ prematureExit :: IOException -> IO ()+ prematureExit err = do modifyMVar_ theirState $ \st -> case st of RemoteEndPointInvalid _ ->@@ -1107,7 +1353,7 @@ -- Release probing resources if probing. forM_ (remoteProbing vst) id let code = EventConnectionLost (remoteAddress theirEndPoint)- writeChan ourChannel . ErrorEvent $ TransportError code (show err)+ qdiscEnqueue' ourQueue theirAddr . ErrorEvent $ TransportError code (show err) return (RemoteEndPointFailed err) RemoteEndPointClosing resolved vst -> do -- Release probing resources if probing.@@ -1117,7 +1363,19 @@ RemoteEndPointClosed -> relyViolation (ourEndPoint, theirEndPoint) "handleIncomingMessages:prematureExit"- RemoteEndPointFailed err' ->+ RemoteEndPointFailed err' -> do+ -- Here we post a connection-lost event, but only if the+ -- local endpoint is not closed; if it's closed, the EndPointClosed+ -- event will be posted without connection-lost events, and this is+ -- part of the network-transport specification (there's a test+ -- case for it).+ modifyMVar_ ourState $ \st' -> case st' of+ LocalEndPointClosed -> return st'+ LocalEndPointValid _ -> do+ let code = EventConnectionLost (remoteAddress theirEndPoint)+ err = TransportError code (show err')+ qdiscEnqueue' ourQueue theirAddr (ErrorEvent err)+ return st' return (RemoteEndPointFailed err') -- Construct a connection ID@@ -1190,7 +1448,10 @@ RemoteEndPointValid vst -> do let connId = vst ^. remoteNextConnOutId act <- schedule theirEndPoint $ do- sendOn vst [encodeInt32 CreatedNewConnection, encodeInt32 connId]+ sendOn vst [+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 connId+ ] return connId return ( RemoteEndPointValid $ remoteNextConnOutId ^= connId + 1@@ -1229,25 +1490,39 @@ (tcpUserTimeout params) connTimeout didAccept <- case result of- Right (sock, ConnectionRequestAccepted) -> do+ -- Since a socket was created, we are now responsible for closing it.+ --+ -- In case the connection was accepted, we have some work to do.+ -- We'll remember how to wait for the socket to close+ -- (readMVar socketClosedVar), and we'll take care of closing it up+ -- once handleIncomingMessages has finished.+ Right (socketClosedVar, sock, ConnectionRequestAccepted) -> do sendLock <- newMVar () let vst = ValidRemoteEndPointState { remoteSocket = sock+ , remoteSocketClosed = readMVar socketClosedVar , remoteProbing = Nothing , remoteSendLock = sendLock , _remoteOutgoing = 0 , _remoteIncoming = Set.empty- , _remoteMaxIncoming = 0+ , _remoteLastIncoming = 0 , _remoteNextConnOutId = firstNonReservedLightweightConnectionId } resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointValid vst)- return True- Right (sock, ConnectionRequestInvalid) -> do+ return (Just (socketClosedVar, sock))+ Right (socketClosedVar, sock, ConnectionRequestUnsupportedVersion) -> do+ -- If the peer doesn't support V0 then there's nothing we can do, for+ -- it's the only version we support.+ let err = connectFailed "setupRemoteEndPoint: unsupported version"+ resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err)+ tryCloseSocket sock `finally` putMVar socketClosedVar ()+ return Nothing+ Right (socketClosedVar, sock, ConnectionRequestInvalid) -> do let err = invalidAddress "setupRemoteEndPoint: Invalid endpoint" resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err)- tryCloseSocket sock- return False- Right (sock, ConnectionRequestCrossed) -> do+ tryCloseSocket sock `finally` putMVar socketClosedVar ()+ return Nothing+ Right (socketClosedVar, sock, ConnectionRequestCrossed) -> do withMVar (remoteState theirEndPoint) $ \st -> case st of RemoteEndPointInit _ crossed _ -> putMVar crossed ()@@ -1255,19 +1530,33 @@ throwIO ex _ -> relyViolation (ourEndPoint, theirEndPoint) "setupRemoteEndPoint: Crossed"- tryCloseSocket sock- return False+ tryCloseSocket sock `finally` putMVar socketClosedVar ()+ return Nothing+ Right (socketClosedVar, sock, ConnectionRequestHostMismatch) -> do+ let handler :: SomeException -> IO (TransportError ConnectErrorCode)+ handler err = return (TransportError ConnectFailed (show err))+ err <- handle handler $ do+ actualHost <- recvWithLength (tcpMaxReceiveLength params) sock+ return (TransportError ConnectFailed ("setupRemoteEndPoint: Host mismatch " ++ BSC.unpack (BS.concat actualHost)))+ resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err)+ tryCloseSocket sock `finally` putMVar socketClosedVar ()+ return Nothing Left err -> do resolveInit (ourEndPoint, theirEndPoint) (RemoteEndPointInvalid err)- return False+ return Nothing - when didAccept $ void $ forkIO $- handleIncomingMessages (ourEndPoint, theirEndPoint)- return $ either (const Nothing) (Just . snd) result+ -- We handle incoming messages in a separate thread, and are careful to+ -- always close the socket once that thread is finished.+ forM_ didAccept $ \(socketClosed, sock) -> void $ forkIO $+ handleIncomingMessages params (ourEndPoint, theirEndPoint)+ `finally`+ (tryCloseSocket sock `finally` putMVar socketClosed ())+ return $ either (const Nothing) (Just . (\(_,_,x) -> x)) result where ourAddress = localAddress ourEndPoint theirAddress = remoteAddress theirEndPoint invalidAddress = TransportError ConnectNotFound+ connectFailed = TransportError ConnectFailed -- | Send a CloseSocket request if the remote endpoint is unused closeIfUnused :: EndPointPair -> IO ()@@ -1278,8 +1567,8 @@ then do resolved <- newEmptyMVar act <- schedule theirEndPoint $- sendOn vst [ encodeInt32 CloseSocket- , encodeInt32 (vst ^. remoteMaxIncoming)+ sendOn vst [ encodeWord32 (encodeControlHeader CloseSocket)+ , encodeWord32 (vst ^. remoteLastIncoming) ] return (RemoteEndPointClosing resolved vst, Just act) else@@ -1324,7 +1613,7 @@ connAlive <- newIORef True -- Protected by the local endpoint lock lconnId <- mapIOException connectFailed $ getLocalNextConnOutId ourEndPoint let connId = createConnectionId heavyweightSelfConnectionId lconnId- writeChan ourChan $+ qdiscEnqueue' ourQueue ourAddress $ ConnectionOpened connId ReliableOrdered (localAddress ourEndPoint) return Connection { send = selfSend connAlive connId@@ -1341,7 +1630,7 @@ alive <- readIORef connAlive if alive then seq (foldr seq () msg)- writeChan ourChan (Received connId msg)+ qdiscEnqueue' ourQueue ourAddress (Received connId msg) else throwIO $ TransportError SendClosed "Connection closed" LocalEndPointClosed -> throwIO $ TransportError SendFailed "Endpoint closed"@@ -1352,14 +1641,15 @@ LocalEndPointValid _ -> do alive <- readIORef connAlive when alive $ do- writeChan ourChan (ConnectionClosed connId)+ qdiscEnqueue' ourQueue ourAddress (ConnectionClosed connId) writeIORef connAlive False LocalEndPointClosed -> return () - ourChan = localChannel ourEndPoint+ ourQueue = localQueue ourEndPoint ourState = localState ourEndPoint connectFailed = TransportError ConnectFailed . show+ ourAddress = localAddress ourEndPoint -- | Resolve an endpoint currently in 'Init' state resolveInit :: EndPointPair -> RemoteState -> IO ()@@ -1397,9 +1687,10 @@ -- -- May throw a TransportError NewEndPointErrorCode exception if the transport -- is closed.-createLocalEndPoint :: TCPTransport -> IO LocalEndPoint-createLocalEndPoint transport = do- chan <- newChan+createLocalEndPoint :: TCPTransport+ -> QDisc Event+ -> IO LocalEndPoint+createLocalEndPoint transport qdisc = do state <- newMVar . LocalEndPointValid $ ValidLocalEndPointState { _localNextConnOutId = firstNonReservedLightweightConnectionId , _localConnections = Map.empty@@ -1411,9 +1702,9 @@ let addr = encodeEndPointAddress (transportHost transport) (transportPort transport) ix- let localEndPoint = LocalEndPoint { localAddress = addr- , localChannel = chan- , localState = state+ let localEndPoint = LocalEndPoint { localAddress = addr+ , localQueue = qdisc+ , localState = state } return ( TransportValid . (localEndPointAt addr ^= Just localEndPoint)@@ -1612,10 +1903,11 @@ handleIOException ex vst = do -- Release probing resources if probing. forM_ (remoteProbing vst) id- tryCloseSocket (remoteSocket vst)- let code = EventConnectionLost (remoteAddress theirEndPoint)- err = TransportError code (show ex)- writeChan (localChannel ourEndPoint) $ ErrorEvent err+ -- Must shut down the socket here, so that the other end will realize+ -- we lost the connection+ tryShutdownSocketBoth (remoteSocket vst)+ -- Eventually, handleIncomingMessages will fail while trying to+ -- receive, and ultimately enqueue the 'EventConnectionLost'. return (RemoteEndPointFailed ex) -- | Use 'schedule' action 'runScheduled' action in a safe way, it's assumed that@@ -1634,6 +1926,11 @@ -- | Establish a connection to a remote endpoint -- -- Maybe throw a TransportError+--+-- If a socket is created and returned (Right is given) then the caller is+-- responsible for eventually closing the socket and filling the MVar (which+-- is empty). The MVar must be filled immediately after, and never before,+-- the socket is closed. socketToEndPoint :: EndPointAddress -- ^ Our address -> EndPointAddress -- ^ Their address -> Bool -- ^ Use SO_REUSEADDR?@@ -1642,7 +1939,7 @@ -> Maybe Int -- ^ Maybe TCP_USER_TIMEOUT -> Maybe Int -- ^ Timeout for connect -> IO (Either (TransportError ConnectErrorCode)- (N.Socket, ConnectionRequestResponse))+ (MVar (), N.Socket, ConnectionRequestResponse)) socketToEndPoint (EndPointAddress ourAddress) theirAddress reuseAddr noDelay keepAlive mUserTimeout timeout = try $ do@@ -1664,12 +1961,17 @@ mapIOException invalidAddress $ N.connect sock (N.addrAddress addr) mapIOException failed $ do- sendMany sock- (encodeInt32 theirEndPointId : prependLength [ourAddress])- recvInt32 sock- case tryToEnum response of+ sendMany sock $+ -- The version.+ encodeWord32 currentProtocolVersion+ -- The V0 handshake data with the length prepended.+ : prependLength (encodeWord32 theirEndPointId : prependLength [ourAddress])+ recvWord32 sock+ case decodeConnectionRequestResponse response of Nothing -> throwIO (failed . userError $ "Unexpected response")- Just r -> return (sock, r)+ Just r -> do+ socketClosedVar <- newEmptyMVar+ return (socketClosedVar, sock, r) where createSocket :: N.AddrInfo -> IO N.Socket createSocket addr = mapIOException insufficientResources $@@ -1680,26 +1982,6 @@ failed = TransportError ConnectFailed . show timeoutError = TransportError ConnectTimeout "Timed out" --- | Encode end point address-encodeEndPointAddress :: N.HostName- -> N.ServiceName- -> EndPointId- -> EndPointAddress-encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $- host ++ ":" ++ port ++ ":" ++ show ix---- | Decode end point address-decodeEndPointAddress :: EndPointAddress- -> Maybe (N.HostName, N.ServiceName, EndPointId)-decodeEndPointAddress (EndPointAddress bs) =- case splitMaxFromEnd (== ':') 2 $ BSC.unpack bs of- [host, port, endPointIdStr] ->- case reads endPointIdStr of- [(endPointId, "")] -> Just (host, port, endPointId)- _ -> Nothing- _ ->- Nothing- -- | Construct a ConnectionId createConnectionId :: HeavyweightConnectionId -> LightweightConnectionId@@ -1707,21 +1989,6 @@ createConnectionId hcid lcid = (fromIntegral hcid `shiftL` 32) .|. fromIntegral lcid --- | @spltiMaxFromEnd p n xs@ splits list @xs@ at elements matching @p@,--- returning at most @p@ segments -- counting from the /end/------ > splitMaxFromEnd (== ':') 2 "ab:cd:ef:gh" == ["ab:cd", "ef", "gh"]-splitMaxFromEnd :: (a -> Bool) -> Int -> [a] -> [[a]]-splitMaxFromEnd p = \n -> go [[]] n . reverse- where- -- go :: [[a]] -> Int -> [a] -> [[a]]- go accs _ [] = accs- go ([] : accs) 0 xs = reverse xs : accs- go (acc : accs) n (x:xs) =- if p x then go ([] : acc : accs) (n - 1) xs- else go ((x : acc) : accs) n xs- go _ _ _ = error "Bug in splitMaxFromEnd"- -------------------------------------------------------------------------------- -- Functions from TransportInternals -- --------------------------------------------------------------------------------@@ -1803,8 +2070,8 @@ remoteIncoming :: Accessor ValidRemoteEndPointState (Set LightweightConnectionId) remoteIncoming = accessor _remoteIncoming (\cs conn -> conn { _remoteIncoming = cs }) -remoteMaxIncoming :: Accessor ValidRemoteEndPointState LightweightConnectionId-remoteMaxIncoming = accessor _remoteMaxIncoming (\lcid st -> st { _remoteMaxIncoming = lcid })+remoteLastIncoming :: Accessor ValidRemoteEndPointState LightweightConnectionId+remoteLastIncoming = accessor _remoteLastIncoming (\lcid st -> st { _remoteLastIncoming = lcid }) remoteNextConnOutId :: Accessor ValidRemoteEndPointState LightweightConnectionId remoteNextConnOutId = accessor _remoteNextConnOutId (\cix st -> st { _remoteNextConnOutId = cix })
src/Network/Transport/TCP/Internal.hs view
@@ -1,18 +1,40 @@ -- | Utility functions for TCP sockets module Network.Transport.TCP.Internal- ( forkServer+ ( ControlHeader(..)+ , encodeControlHeader+ , decodeControlHeader+ , ConnectionRequestResponse(..)+ , encodeConnectionRequestResponse+ , decodeConnectionRequestResponse+ , forkServer , recvWithLength , recvExact- , recvInt32+ , recvWord32+ , encodeWord32 , tryCloseSocket+ , tryShutdownSocketBoth+ , decodeSockAddr+ , EndPointId+ , encodeEndPointAddress+ , decodeEndPointAddress+ , ProtocolVersion+ , currentProtocolVersion ) where #if ! MIN_VERSION_base(4,6,0) import Prelude hiding (catch) #endif -import Network.Transport.Internal (decodeInt32, void, tryIO, forkIOWithUnmask)+import Network.Transport.Internal+ ( decodeWord32+ , encodeWord32+ , void+ , tryIO+ , forkIOWithUnmask+ ) +import Network.Transport ( EndPointAddress(..) )+ #ifdef USE_MOCK_NETWORK import qualified Network.Transport.TCP.Mock.Socket as N #else@@ -35,6 +57,10 @@ , accept , sClose , socketPort+ , shutdown+ , ShutdownCmd(ShutdownBoth)+ , SockAddr(..)+ , inet_ntoa ) #ifdef USE_MOCK_NETWORK@@ -43,44 +69,150 @@ import qualified Network.Socket.ByteString as NBS (recv) #endif -import Control.Concurrent (ThreadId)+import Data.Word (Word32)+ import Control.Monad (forever, when) import Control.Exception (SomeException, catch, bracketOnError, throwIO, mask_)+import Control.Concurrent (ThreadId, forkIO)+import Control.Concurrent.MVar+ ( MVar+ , newEmptyMVar+ , putMVar+ , readMVar+ )+import Control.Monad (forever, when)+import Control.Exception+ ( SomeException+ , catch+ , bracketOnError+ , throwIO+ , mask_+ , mask+ , finally+ , onException+ )+ import Control.Applicative ((<$>), (<*>))+import Data.Word (Word32) import Data.ByteString (ByteString) import qualified Data.ByteString as BS (length, concat, null)-import Data.Int (Int32) import Data.ByteString.Lazy.Internal (smallChunkSize)+import qualified Data.ByteString.Char8 as BSC (unpack, pack) +-- | Local identifier for an endpoint within this transport+type EndPointId = Word32++-- | Identifies the version of the network-transport-tcp protocol.+-- It's the first piece of data sent when a new heavyweight connection is+-- established.+type ProtocolVersion = Word32++currentProtocolVersion :: ProtocolVersion+currentProtocolVersion = 0x00000000++-- | Control headers+data ControlHeader =+ -- | Tell the remote endpoint that we created a new connection+ CreatedNewConnection+ -- | Tell the remote endpoint we will no longer be using a connection+ | CloseConnection+ -- | Request to close the connection (see module description)+ | CloseSocket+ -- | Sent by an endpoint when it is closed.+ | CloseEndPoint+ -- | Message sent to probe a socket+ | ProbeSocket+ -- | Acknowledgement of the ProbeSocket message+ | ProbeSocketAck+ deriving (Show)++decodeControlHeader :: Word32 -> Maybe ControlHeader+decodeControlHeader w32 = case w32 of+ 0 -> Just CreatedNewConnection+ 1 -> Just CloseConnection+ 2 -> Just CloseSocket+ 3 -> Just CloseEndPoint+ 4 -> Just ProbeSocket+ 5 -> Just ProbeSocketAck+ _ -> Nothing++encodeControlHeader :: ControlHeader -> Word32+encodeControlHeader ch = case ch of+ CreatedNewConnection -> 0+ CloseConnection -> 1+ CloseSocket -> 2+ CloseEndPoint -> 3+ ProbeSocket -> 4+ ProbeSocketAck -> 5++-- | Response sent by /B/ to /A/ when /A/ tries to connect+data ConnectionRequestResponse =+ -- | /B/ does not support the protocol version requested by /A/.+ ConnectionRequestUnsupportedVersion+ -- | /B/ accepts the connection+ | ConnectionRequestAccepted+ -- | /A/ requested an invalid endpoint+ | ConnectionRequestInvalid+ -- | /A/s request crossed with a request from /B/ (see protocols)+ | ConnectionRequestCrossed+ -- | /A/ gave an incorrect host (did not match the host that /B/ observed).+ | ConnectionRequestHostMismatch+ deriving (Show)++decodeConnectionRequestResponse :: Word32 -> Maybe ConnectionRequestResponse+decodeConnectionRequestResponse w32 = case w32 of+ 0xFFFFFFFF -> Just ConnectionRequestUnsupportedVersion+ 0x00000000 -> Just ConnectionRequestAccepted+ 0x00000001 -> Just ConnectionRequestInvalid+ 0x00000002 -> Just ConnectionRequestCrossed+ 0x00000003 -> Just ConnectionRequestHostMismatch+ _ -> Nothing++encodeConnectionRequestResponse :: ConnectionRequestResponse -> Word32+encodeConnectionRequestResponse crr = case crr of+ ConnectionRequestUnsupportedVersion -> 0xFFFFFFFF+ ConnectionRequestAccepted -> 0x00000000+ ConnectionRequestInvalid -> 0x00000001+ ConnectionRequestCrossed -> 0x00000002+ ConnectionRequestHostMismatch -> 0x00000003+ -- | Start a server at the specified address. -- -- This sets up a server socket for the specified host and port. Exceptions -- thrown during setup are not caught. -- -- Once the socket is created we spawn a new thread which repeatedly accepts--- incoming connections and executes the given request handler. If any--- exception occurs the thread terminates and calls the terminationHandler.--- This exception may occur because of a call to 'N.accept', because the thread--- was explicitly killed, or because of a synchronous exception thrown by the--- request handler. Typically, you should avoid the last case by catching any--- relevant exceptions in the request handler.+-- incoming connections and executes the given request handler in another+-- thread. If any exception occurs the accepting thread terminates and calls+-- the terminationHandler. Threads spawned for previous accepted connections+-- are not killed.+-- This exception may occur because of a call to 'N.accept', or because the+-- thread was explicitly killed. ----- The request handler should spawn threads to handle each individual request--- or the server will block. Once a thread has been spawned it will be the--- responsibility of the new thread to close the socket when an exception--- occurs.+-- The request handler is not responsible for closing the socket. It will be+-- closed once that handler returns. Take care to ensure that the socket is not+-- used after the handler returns, or you will get undefined behavior+-- (the file descriptor may be re-used). -- -- The return value includes the port was bound to. This is not always the same -- port as that given in the argument. For example, binding to port 0 actually -- binds to a random port, selected by the OS.-forkServer :: N.HostName -- ^ Host- -> N.ServiceName -- ^ Port- -> Int -- ^ Backlog (maximum number of queued connections)- -> Bool -- ^ Set ReuseAddr option?- -> (SomeException -> IO ()) -- ^ Termination handler- -> (N.Socket -> IO ()) -- ^ Request handler+forkServer :: N.HostName -- ^ Host+ -> N.ServiceName -- ^ Port+ -> Int -- ^ Backlog (maximum number of queued connections)+ -> Bool -- ^ Set ReuseAddr option?+ -> (SomeException -> IO ()) -- ^ Error handler. Called with an+ -- exception raised when+ -- accepting a connection.+ -> (SomeException -> IO ()) -- ^ Termination handler. Called+ -- when the error handler throws+ -- an exception.+ -> (IO () -> (N.Socket, N.SockAddr) -> IO ())+ -- ^ Request handler. Gets an+ -- action which completes when+ -- the socket is closed. -> IO (N.ServiceName, ThreadId)-forkServer host port backlog reuseAddr terminationHandler requestHandler = do+forkServer host port backlog reuseAddr errorHandler terminationHandler requestHandler = do -- Resolve the specified address. By specification, getAddrInfo will never -- return an empty list (but will throw an exception instead) and will return -- the "best" address first, whatever that means@@ -90,50 +222,129 @@ when reuseAddr $ N.setSocketOption sock N.ReuseAddr 1 N.bindSocket sock (N.addrAddress addr) N.listen sock backlog++ -- Close up and fill the synchonizing MVar.+ let release :: ((N.Socket, N.SockAddr), MVar ()) -> IO ()+ release ((sock, _), socketClosed) =+ N.sClose sock `finally` putMVar socketClosed ()++ -- Run the request handler.+ let act restore (sock, sockAddr) = do+ socketClosed <- newEmptyMVar+ void $ forkIO $ restore $ do+ requestHandler (readMVar socketClosed) (sock, sockAddr)+ `finally`+ release ((sock, sockAddr), socketClosed)++ let acceptRequest :: IO ()+ acceptRequest = mask $ \restore -> do+ -- Async exceptions are masked so that, if accept does give a+ -- socket, we'll always deliver it to the handler before the+ -- exception is raised.+ -- If it's a Right handler then it will eventually be closed.+ -- If it's a Left handler then we assume the handler itself will+ -- close it.+ (sock, sockAddr) <- N.accept sock+ -- Looks like 'act' will never throw an exception, but to be+ -- safe we'll close the socket if it does.+ let handler :: SomeException -> IO ()+ handler _ = N.sClose sock+ catch (act restore (sock, sockAddr)) handler+ -- We start listening for incoming requests in a separate thread. When -- that thread is killed, we close the server socket and the termination- -- handler. We have to make sure that the exception handler is installed- -- /before/ any asynchronous exception occurs. So we mask_, then fork- -- (the child thread inherits the masked state from the parent), then+ -- handler is run. We have to make sure that the exception handler is+ -- installed /before/ any asynchronous exception occurs. So we mask_, then+ -- fork (the child thread inherits the masked state from the parent), then -- unmask only inside the catch. (,) <$> fmap show (N.socketPort sock) <*> (mask_ $ forkIOWithUnmask $ \unmask ->- catch (unmask (forever $ acceptRequest sock)) $ \ex -> do+ catch (unmask (forever (catch acceptRequest errorHandler))) $ \ex -> do tryCloseSocket sock terminationHandler ex)- where- acceptRequest :: N.Socket -> IO ()- acceptRequest sock = bracketOnError (N.accept sock)- (tryCloseSocket . fst)- (requestHandler . fst) --- | Read a length and then a payload of that length-recvWithLength :: N.Socket -> IO [ByteString]-recvWithLength sock = recvInt32 sock >>= recvExact sock+-- | Read a length and then a payload of that length, subject to a limit+-- on the length.+-- If the length (first 'Word32' received) is greater than the limit then+-- an exception is thrown.+recvWithLength :: Word32 -> N.Socket -> IO [ByteString]+recvWithLength limit sock = do+ len <- recvWord32 sock+ when (len > limit) $+ throwIO (userError "recvWithLength: limit exceeded")+ recvExact sock len --- | Receive a 32-bit integer-recvInt32 :: Num a => N.Socket -> IO a-recvInt32 sock = decodeInt32 . BS.concat <$> recvExact sock 4+-- | Receive a 32-bit unsigned integer+recvWord32 :: N.Socket -> IO Word32+recvWord32 = fmap (decodeWord32 . BS.concat) . flip recvExact 4 --- | Close a socket, ignoring I/O exceptions+-- | Close a socket, ignoring I/O exceptions. tryCloseSocket :: N.Socket -> IO () tryCloseSocket sock = void . tryIO $ N.sClose sock +-- | Shutdown socket sends and receives, ignoring I/O exceptions.+tryShutdownSocketBoth :: N.Socket -> IO ()+tryShutdownSocketBoth sock = void . tryIO $+ N.shutdown sock N.ShutdownBoth+ -- | Read an exact number of bytes from a socket -- -- Throws an I/O exception if the socket closes before the specified -- number of bytes could be read-recvExact :: N.Socket -- ^ Socket to read from- -> Int32 -- ^ Number of bytes to read- -> IO [ByteString]-recvExact _ len | len < 0 = throwIO (userError "recvExact: Negative length")+recvExact :: N.Socket -- ^ Socket to read from+ -> Word32 -- ^ Number of bytes to read+ -> IO [ByteString] -- ^ Data read recvExact sock len = go [] len where- go :: [ByteString] -> Int32 -> IO [ByteString]+ go :: [ByteString] -> Word32 -> IO [ByteString] go acc 0 = return (reverse acc) go acc l = do bs <- NBS.recv sock (fromIntegral l `min` smallChunkSize) if BS.null bs then throwIO (userError "recvExact: Socket closed") else go (bs : acc) (l - fromIntegral (BS.length bs))++-- | Produce a HostName and ServiceName from a SockAddr. Only gives 'Just' for+-- IPv4 addresses.+decodeSockAddr :: N.SockAddr -> IO (Maybe (N.HostName, N.ServiceName))+decodeSockAddr sockAddr = case sockAddr of+ N.SockAddrInet port host -> do+ hostString <- N.inet_ntoa host+ return $ Just (hostString, show port)+ _ -> return Nothing++-- | Encode end point address+encodeEndPointAddress :: N.HostName+ -> N.ServiceName+ -> EndPointId+ -> EndPointAddress+encodeEndPointAddress host port ix = EndPointAddress . BSC.pack $+ host ++ ":" ++ port ++ ":" ++ show ix++-- | Decode end point address+decodeEndPointAddress :: EndPointAddress+ -> Maybe (N.HostName, N.ServiceName, EndPointId)+decodeEndPointAddress (EndPointAddress bs) =+ case splitMaxFromEnd (== ':') 2 $ BSC.unpack bs of+ [host, port, endPointIdStr] ->+ case reads endPointIdStr of+ [(endPointId, "")] -> Just (host, port, endPointId)+ _ -> Nothing+ _ ->+ Nothing++-- | @spltiMaxFromEnd p n xs@ splits list @xs@ at elements matching @p@,+-- returning at most @p@ segments -- counting from the /end/+--+-- > splitMaxFromEnd (== ':') 2 "ab:cd:ef:gh" == ["ab:cd", "ef", "gh"]+splitMaxFromEnd :: (a -> Bool) -> Int -> [a] -> [[a]]+splitMaxFromEnd p = \n -> go [[]] n . reverse+ where+ -- go :: [[a]] -> Int -> [a] -> [[a]]+ go accs _ [] = accs+ go ([] : accs) 0 xs = reverse xs : accs+ go (acc : accs) n (x:xs) =+ if p x then go ([] : acc : accs) (n - 1) xs+ else go ((x : acc) : accs) n xs+ go _ _ _ = error "Bug in splitMaxFromEnd"
tests/TestTCP.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE RebindableSyntax, TemplateHaskell #-}+{-# LANGUAGE BangPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where @@ -15,11 +16,10 @@ import Network.Transport.TCP ( createTransport , createTransportExposeInternals , TransportInternals(..)- , encodeEndPointAddress+ , TCPParameters(..) , defaultTCPParameters , LightweightConnectionId )-import Data.Int (Int32) import Control.Concurrent (threadDelay, killThread) import Control.Concurrent.MVar ( MVar , newEmptyMVar@@ -30,21 +30,31 @@ , newMVar , modifyMVar , modifyMVar_+ , swapMVar ) import Control.Monad (replicateM, guard, forM_, replicateM_, when) import Control.Applicative ((<$>)) import Control.Exception (throwIO, try, SomeException)-import Network.Transport.TCP ( ControlHeader(..)- , ConnectionRequestResponse(..)- , socketToEndPoint- )-import Network.Transport.Internal ( encodeInt32- , prependLength+import Network.Transport.TCP ( socketToEndPoint )+import Network.Transport.Internal ( prependLength , tlog , tryIO , void )-import Network.Transport.TCP.Internal (recvInt32, forkServer, recvWithLength)+import Network.Transport.TCP.Internal+ ( ControlHeader(..)+ , encodeControlHeader+ , decodeControlHeader+ , ConnectionRequestResponse(..)+ , encodeConnectionRequestResponse+ , decodeConnectionRequestResponse+ , encodeWord32+ , recvWord32+ , forkServer+ , recvWithLength+ , encodeEndPointAddress+ , decodeEndPointAddress+ ) #ifdef USE_MOCK_NETWORK import qualified Network.Transport.TCP.Mock.Socket as N@@ -57,6 +67,15 @@ , AddrInfo , shutdown , ShutdownCmd(ShutdownSend)+ , SockAddr(..)+ , SocketType(Stream)+ , AddrInfo(..)+ , getAddrInfo+ , defaultHints+ , defaultProtocol+ , socket+ , connect+ , close ) #ifdef USE_MOCK_NETWORK@@ -65,6 +84,7 @@ import Network.Socket.ByteString (sendMany) #endif +import qualified Data.ByteString as BS (length, concat) import Data.String (fromString) import GHC.IO.Exception (ioe_errno) import Foreign.C.Error (Errno(..), eADDRNOTAVAIL)@@ -105,7 +125,7 @@ server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () server serverAddr clientAddr serverDone = do tlog "Server"- Right transport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right endpoint <- newEndPoint transport putMVar serverAddr (address endpoint) theirAddr <- readMVar clientAddr@@ -154,23 +174,26 @@ tlog "Client" -- Listen for incoming messages- (clientPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO $ \sock -> do+ (clientPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO throwIO $ \socketFree (sock, _) -> do -- Initial setup- 0 <- recvInt32 sock :: IO Int- _ <- recvWithLength sock- sendMany sock [encodeInt32 ConnectionRequestAccepted]+ 0 <- recvWord32 sock+ _ <- recvWithLength maxBound sock+ sendMany sock [encodeWord32 (encodeConnectionRequestResponse ConnectionRequestAccepted)] -- Server opens a logical connection- CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int)- 1024 <- recvInt32 sock :: IO LightweightConnectionId+ Just CreatedNewConnection <- decodeControlHeader <$> recvWord32 sock+ 1024 <- recvWord32 sock :: IO LightweightConnectionId -- Server sends a message- 1024 <- recvInt32 sock :: IO Int- ["ping"] <- recvWithLength sock+ 1024 <- recvWord32 sock+ ["ping"] <- recvWithLength maxBound sock -- Reply- sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10002 :: Int)]- sendMany sock (encodeInt32 10002 : prependLength ["pong"])+ sendMany sock [+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 10002+ ]+ sendMany sock (encodeWord32 10002 : prependLength ["pong"]) -- Close the socket N.sClose sock@@ -179,10 +202,13 @@ putMVar clientAddr ourAddress -- Connect to the server- Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True False False Nothing Nothing+ Right (_, sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True False False Nothing Nothing -- Open a new connection- sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10003 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 10003+ ] -- Close the socket without closing the connection explicitly -- The server should receive an error event@@ -204,7 +230,7 @@ server :: MVar EndPointAddress -> MVar EndPointAddress -> MVar () -> IO () server serverAddr clientAddr serverDone = do tlog "Server"- Right transport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right endpoint <- newEndPoint transport putMVar serverAddr (address endpoint) theirAddr <- readMVar clientAddr@@ -260,53 +286,65 @@ tlog "Client" -- Listen for incoming messages- (clientPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO $ \sock -> do+ (clientPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO throwIO $ \socketFree (sock, _) -> do -- Initial setup- 0 <- recvInt32 sock :: IO Int- _ <- recvWithLength sock- sendMany sock [encodeInt32 ConnectionRequestAccepted]+ 0 <- recvWord32 sock+ _ <- recvWithLength maxBound sock+ sendMany sock [encodeWord32 (encodeConnectionRequestResponse ConnectionRequestAccepted)] -- Server opens a logical connection- CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int)- 1024 <- recvInt32 sock :: IO LightweightConnectionId+ Just CreatedNewConnection <- decodeControlHeader <$> recvWord32 sock+ 1024 <- recvWord32 sock :: IO LightweightConnectionId -- Server sends a message- 1024 <- recvInt32 sock :: IO Int- ["ping"] <- recvWithLength sock+ 1024 <- recvWord32 sock+ ["ping"] <- recvWithLength maxBound sock -- Reply- sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10002 :: Int)]- sendMany sock (encodeInt32 (10002 :: Int) : prependLength ["pong"])+ sendMany sock [+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 10002+ ]+ sendMany sock (encodeWord32 10002 : prependLength ["pong"]) -- Send a CloseSocket even though there are still connections *in both -- directions*- sendMany sock [encodeInt32 CloseSocket, encodeInt32 (1024 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CloseSocket)+ , encodeWord32 1024+ ] N.sClose sock let ourAddress = encodeEndPointAddress "127.0.0.1" clientPort 0 putMVar clientAddr ourAddress -- Connect to the server- Right (sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True False False Nothing Nothing+ Right (_, sock, ConnectionRequestAccepted) <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True False False Nothing Nothing -- Open a new connection- sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (10003 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 10003+ ] -- Send a CloseSocket without sending a closeconnecton -- The server should still receive a ConnectionClosed message- sendMany sock [encodeInt32 CloseSocket, encodeInt32 (0 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CloseSocket)+ , encodeWord32 0+ ] N.sClose sock -- | Test the creation of a transport with an invalid address testInvalidAddress :: IO () testInvalidAddress = do- Left _ <- createTransport "invalidHostName" "0" defaultTCPParameters+ Left _ <- createTransport "invalidHostName" "0" ((,) "invalidHostName") defaultTCPParameters return () -- | Test connecting to invalid or non-existing endpoints testInvalidConnect :: IO () testInvalidConnect = do- Right transport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right endpoint <- newEndPoint transport -- Syntax error in the endpoint address@@ -337,7 +375,7 @@ clientDone <- newEmptyMVar serverDone <- newEmptyMVar connectionEstablished <- newEmptyMVar- Right transport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters -- Server forkTry $ do@@ -375,32 +413,41 @@ theirAddress <- readMVar serverAddr -- Connect to the server- Right (sock, ConnectionRequestAccepted) <- socketToEndPoint ourAddress theirAddress True False False Nothing Nothing+ Right (_, sock, ConnectionRequestAccepted) <- socketToEndPoint ourAddress theirAddress True False False Nothing Nothing putMVar connectionEstablished () -- Server connects to us, and then closes the connection- CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int)- 1024 <- recvInt32 sock :: IO LightweightConnectionId+ Just CreatedNewConnection <- decodeControlHeader <$> recvWord32 sock+ 1024 <- recvWord32 sock :: IO LightweightConnectionId - CloseConnection <- toEnum <$> (recvInt32 sock :: IO Int)- 1024 <- recvInt32 sock :: IO LightweightConnectionId+ Just CloseConnection <- decodeControlHeader <$> recvWord32 sock+ 1024 <- recvWord32 sock :: IO LightweightConnectionId -- Server will now send a CloseSocket request as its refcount reached 0 tlog "Waiting for CloseSocket request"- CloseSocket <- toEnum <$> recvInt32 sock- _ <- recvInt32 sock :: IO LightweightConnectionId+ Just CloseSocket <- decodeControlHeader <$> recvWord32 sock+ _ <- recvWord32 sock :: IO LightweightConnectionId -- But we ignore it and request another connection in the other direction tlog "Ignoring it, requesting another connection"- sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (1024 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 1024+ ] -- Close it again tlog "Closing connection"- sendMany sock [encodeInt32 CloseConnection, encodeInt32 (1024 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CloseConnection)+ , encodeWord32 1024+ ] -- And close the connection completely tlog "Closing socket"- sendMany sock [encodeInt32 CloseSocket, encodeInt32 (1024 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CloseSocket)+ , encodeWord32 1024+ ] N.sClose sock putMVar clientDone ()@@ -418,7 +465,7 @@ clientDone <- newEmptyMVar serverDone <- newEmptyMVar connectionEstablished <- newEmptyMVar- Right transport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters -- Server forkTry $ do@@ -452,20 +499,20 @@ theirAddress <- readMVar serverAddr -- Connect to the server- Right (sock, ConnectionRequestAccepted) <- socketToEndPoint ourAddress theirAddress True False False Nothing Nothing+ Right (_, sock, ConnectionRequestAccepted) <- socketToEndPoint ourAddress theirAddress True False False Nothing Nothing putMVar connectionEstablished () -- Server connects to us, and then closes the connection- CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int)- 1024 <- recvInt32 sock :: IO LightweightConnectionId+ Just CreatedNewConnection <- decodeControlHeader <$> recvWord32 sock+ 1024 <- recvWord32 sock :: IO LightweightConnectionId - CloseConnection <- toEnum <$> (recvInt32 sock :: IO Int)- 1024 <- recvInt32 sock :: IO LightweightConnectionId+ Just CloseConnection <- decodeControlHeader <$> recvWord32 sock+ 1024 <- recvWord32 sock :: IO LightweightConnectionId -- Server will now send a CloseSocket request as its refcount reached 0 tlog "Waiting for CloseSocket request"- CloseSocket <- toEnum <$> recvInt32 sock- _ <- recvInt32 sock :: IO LightweightConnectionId+ Just CloseSocket <- decodeControlHeader <$> recvWord32 sock+ _ <- recvWord32 sock :: IO LightweightConnectionId unblocked <- newMVar False @@ -473,7 +520,7 @@ -- responding to the CloseSocket request (in this case, we -- respond by sending a ConnectionRequest) forkTry $ do- recvInt32 sock :: IO Int32+ recvWord32 sock readMVar unblocked >>= guard putMVar clientDone () @@ -482,7 +529,10 @@ tlog "Client ignores close socket and sends connection request" tlog "This should unblock the server" modifyMVar_ unblocked $ \_ -> return True- sendMany sock [encodeInt32 CreatedNewConnection, encodeInt32 (1024 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 1024+ ] takeMVar clientDone takeMVar serverDone@@ -495,13 +545,18 @@ serverAddr <- newEmptyMVar forkTry $ do- Right transport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport <- createTransport "127.0.0.1" "0" ((,) "128.0.0.1") defaultTCPParameters Right endpoint <- newEndPoint transport- putMVar serverAddr (address endpoint)+ -- Since we're lying about the server's address, we have to manually+ -- construct the proper address. If we used its actual address, the clients+ -- would try to resolve "128.0.0.1" and then would fail due to invalid+ -- address.+ Just (_, port, epid) <- return $ decodeEndPointAddress (address endpoint)+ putMVar serverAddr $ encodeEndPointAddress "127.0.0.1" port epid forkTry $ do- -- We pick an address < 127.0.0.1 so that this is not rejected purely because of the "crossed" check- let ourAddress = EndPointAddress "126.0.0.1"+ -- We pick an address < 128.0.0.1 so that this is not rejected purely because of the "crossed" check+ let ourAddress = encodeEndPointAddress "127.0.0.1" "1234" 0 -- We should only get a single 'Accepted' reply gotAccepted <- newEmptyMVar@@ -512,14 +567,14 @@ -- immediately (depending on far the remote endpoint got with the initialization) response <- readMVar serverAddr >>= \addr -> socketToEndPoint ourAddress addr True False False Nothing Nothing case response of- Right (_, ConnectionRequestAccepted) ->+ Right (_, _, ConnectionRequestAccepted) -> -- We don't close this socket because we want to keep this connection open putMVar gotAccepted () -- We might get either Invalid or Crossed (the transport does not -- maintain enough history to be able to tell)- Right (sock, ConnectionRequestInvalid) ->+ Right (_, sock, ConnectionRequestInvalid) -> N.sClose sock- Right (sock, ConnectionRequestCrossed) ->+ Right (_, sock, ConnectionRequestCrossed) -> N.sClose sock Left _ -> return ()@@ -534,11 +589,11 @@ -- | Test that we can create "many" transport instances testMany :: IO () testMany = do- Right masterTransport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right masterTransport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right masterEndPoint <- newEndPoint masterTransport replicateM_ 10 $ do- mTransport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ mTransport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters case mTransport of Left ex -> do putStrLn $ "IOException: " ++ show ex ++ "; errno = " ++ show (ioe_errno ex)@@ -555,7 +610,7 @@ -- | Test what happens when the transport breaks completely testBreakTransport :: IO () testBreakTransport = do- Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "0" defaultTCPParameters+ Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right endpoint <- newEndPoint transport killThread (transportThread internals) -- Uh oh@@ -564,6 +619,13 @@ return () +-- Used in testReconnect to block until a socket is closed. newtype is needed+-- for the Traceable instance.+newtype WaitSocketFree = WaitSocketFree (IO ())++instance Traceable WaitSocketFree where+ trace = const Nothing+ -- | Test that a second call to 'connect' might succeed even if the first -- failed. This is a TCP specific test rather than an endpoint specific test -- because we must manually create the endpoint address to match an endpoint we@@ -575,72 +637,110 @@ testReconnect = do serverDone <- newEmptyMVar endpointCreated <- newEmptyMVar+ -- The server will put the 'socketFree' IO in here, so that the client can+ -- block until the server has closed the socket.+ socketClosed <- newEmptyMVar counter <- newMVar (0 :: Int) -- Server- (serverPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO $ \sock -> do+ (serverPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO throwIO $ \socketFree (sock, _) -> do -- Accept the connection- Right 0 <- tryIO $ (recvInt32 sock :: IO Int)- Right _ <- tryIO $ recvWithLength sock+ Right 0 <- tryIO $ recvWord32 sock+ Right _ <- tryIO $ recvWithLength maxBound sock -- The first time we close the socket before accepting the logical connection count <- modifyMVar counter $ \i -> return (i + 1, i) + -- Wait 100ms after the socket closes, to (hopefully) ensure that the client+ -- knows the connection is closed, and sending on that socket will therefore+ -- fail.+ putMVar socketClosed (WaitSocketFree (socketFree >> threadDelay 100000))+ when (count > 0) $ do- Right () <- tryIO $ sendMany sock [encodeInt32 ConnectionRequestAccepted]+ -- The second, third, and fourth connections are accepted according to the+ -- protocol.+ -- On the second request, the socket then closes.+ Right () <- tryIO $ sendMany sock [+ encodeWord32 (encodeConnectionRequestResponse ConnectionRequestAccepted)+ ] -- Client requests a logical connection when (count > 1) $ do- Right CreatedNewConnection <- tryIO $ toEnum <$> (recvInt32 sock :: IO Int)- connId <- recvInt32 sock :: IO LightweightConnectionId- return ()+ -- On the third and fourth requests, a new logical connection is+ -- accepted.+ -- On the third request the socket then closes.+ Right (Just CreatedNewConnection) <- tryIO $ decodeControlHeader <$> recvWord32 sock+ connId <- recvWord32 sock :: IO LightweightConnectionId when (count > 2) $ do- -- Client sends a message- Right connId' <- tryIO $ (recvInt32 sock :: IO LightweightConnectionId)+ -- On the fourth request, a message is received and then the socket+ -- is closed.+ Right connId' <- tryIO $ (recvWord32 sock :: IO LightweightConnectionId) True <- return $ connId == connId'- Right ["ping"] <- tryIO $ recvWithLength sock+ Right ["ping"] <- tryIO $ recvWithLength maxBound sock putMVar serverDone () - Right () <- tryIO $ N.sClose sock return () putMVar endpointCreated () -- Client forkTry $ do- Right transport <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right endpoint <- newEndPoint transport let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 - -- The second attempt will fail because the server closes the socket before we can request a connection takeMVar endpointCreated- -- This might time out or not, depending on whether the server closes the- -- socket before or after we can send the RequestConnectionId request- resultConnect <- timeout 500000 $ connect endpoint theirAddr ReliableOrdered defaultConnectHints++ -- First attempt: fails because the server closes the socket without+ -- doing the handshake.+ resultConnect <- connect endpoint theirAddr ReliableOrdered defaultConnectHints case resultConnect of- Nothing -> return ()- Just (Left (TransportError ConnectFailed _)) -> return ()- Just (Left err) -> throwIO err- Just (Right _) -> throwIO $ userError "testConnect: unexpected connect success"+ Left (TransportError ConnectFailed _) -> return ()+ Left err -> throwIO err+ Right _ -> throwIO $ userError "testConnect: unexpected connect success"+ WaitSocketFree wait <- takeMVar socketClosed+ wait - resultConnect <- timeout 500000 $ connect endpoint theirAddr ReliableOrdered defaultConnectHints+ -- Second attempt: server accepts the connection but then closes the socket.+ -- We expect a failed connection if the socket is closed *before*+ -- CreatedNewConnection is sent, or a successful connection such that a+ -- subsequent send will fail in case CreatedNewConnection was sent before+ -- the close.+ resultConnect <- connect endpoint theirAddr ReliableOrdered defaultConnectHints+ -- We must be sure that the socket has closed before trying to send.+ WaitSocketFree wait <- takeMVar socketClosed+ wait case resultConnect of- Nothing -> return ()- Just (Left (TransportError ConnectFailed _)) -> return ()- Just (Left err) -> throwIO err- Just (Right c) -> do- ev <- send c ["foo"]+ Left (TransportError ConnectFailed _) -> return ()+ Left err -> throwIO err+ Right c -> do+ ev <- send c ["ping"] case ev of Left _ -> return () Right _ -> throwIO $ userError "testConnect: unexpected send success" - -- The third attempt succeeds- Right conn1 <- connect endpoint theirAddr ReliableOrdered defaultConnectHints+ -- In any case, since a heavyweight connection was made, we'll get a+ -- connection lost event.+ ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint - -- But a send will fail because the server has closed the connection again- threadDelay 100000- Left (TransportError SendFailed _) <- send conn1 ["ping"]+ -- Third attempt: server accepts the heavyweight and the lightweight+ -- connection (CreatedNewConnection) but then closes the socket.+ -- The connection must succeed, but sending after the socket is closed+ -- must fail.+ resultConnect <- connect endpoint theirAddr ReliableOrdered defaultConnectHints+ -- Wait until close before trying to send.+ WaitSocketFree wait <- takeMVar socketClosed+ wait+ case resultConnect of+ Left err -> throwIO err+ Right c -> do+ ev <- send c ["ping"]+ case ev of+ Left (TransportError SendFailed _) -> return ()+ Left err -> throwIO err+ Right _ -> throwIO $ userError "testConnect: unexpected send success"+ ErrorEvent (TransportError (EventConnectionLost _) _) <- receive endpoint -- But a subsequent call to connect should reestablish the connection@@ -648,6 +748,9 @@ -- Send should now succeed Right () <- send conn2 ["ping"]++ WaitSocketFree wait <- takeMVar socketClosed+ wait return () takeMVar serverDone@@ -662,28 +765,32 @@ serverGotPing <- newEmptyMVar -- Server- (serverPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO $ \sock -> do+ (serverPort, _) <- forkServer "127.0.0.1" "0" 5 True throwIO throwIO $ \socketFree (sock, _) -> do -- We accept connections, but when an exception occurs we don't do -- anything (in particular, we don't close the socket). This is important -- because when we shutdown one direction of the socket a recv here will -- fail, but we don't want to close that socket at that point (which -- would shutdown the socket in the other direction) void . (try :: IO () -> IO (Either SomeException ())) $ do- 0 <- recvInt32 sock :: IO Int- _ <- recvWithLength sock- () <- sendMany sock [encodeInt32 ConnectionRequestAccepted]+ 0 <- recvWord32 sock+ _ <- recvWithLength maxBound sock+ () <- sendMany sock [encodeWord32 (encodeConnectionRequestResponse ConnectionRequestAccepted)] - CreatedNewConnection <- toEnum <$> (recvInt32 sock :: IO Int)- connId <- recvInt32 sock :: IO LightweightConnectionId+ Just CreatedNewConnection <- decodeControlHeader <$> recvWord32 sock+ connId <- recvWord32 sock :: IO LightweightConnectionId - connId' <- recvInt32 sock :: IO LightweightConnectionId+ connId' <- recvWord32 sock :: IO LightweightConnectionId True <- return $ connId == connId'- ["ping"] <- recvWithLength sock+ ["ping"] <- recvWithLength maxBound sock putMVar serverGotPing () + -- Must read the clientDone MVar so that we don't close the socket+ -- (forkServer will close it once this action ends).+ readMVar clientDone+ -- Client forkTry $ do- Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "0" defaultTCPParameters+ Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right endpoint <- newEndPoint transport let theirAddr = encodeEndPointAddress "127.0.0.1" serverPort 0 @@ -734,11 +841,11 @@ putMVar clientDone () - takeMVar clientDone+ readMVar clientDone testInvalidCloseConnection :: IO () testInvalidCloseConnection = do- Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "0" defaultTCPParameters+ Right (transport, internals) <- createTransportExposeInternals "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters serverAddr <- newEmptyMVar clientDone <- newEmptyMVar serverDone <- newEmptyMVar@@ -767,7 +874,10 @@ -- Get a handle on the TCP connection and manually send an invalid CloseConnection request sock <- socketBetween internals ourAddr theirAddr- sendMany sock [encodeInt32 CloseConnection, encodeInt32 (12345 :: Int)]+ sendMany sock [+ encodeWord32 (encodeControlHeader CloseConnection)+ , encodeWord32 (12345 :: LightweightConnectionId)+ ] putMVar clientDone () @@ -777,19 +887,157 @@ testUseRandomPort = do testDone <- newEmptyMVar forkTry $ do- Right transport1 <- createTransport "127.0.0.1" "0" defaultTCPParameters+ Right transport1 <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters Right ep1 <- newEndPoint transport1- Right transport2 <- createTransport "127.0.0.1" "0" defaultTCPParameters+ -- Same as transport1, but is strict in the port.+ Right transport2 <- createTransport "127.0.0.1" "0" (\(!port) -> ("127.0.0.1", port)) defaultTCPParameters Right ep2 <- newEndPoint transport2 Right conn1 <- connect ep2 (address ep1) ReliableOrdered defaultConnectHints ConnectionOpened _ _ _ <- receive ep1 putMVar testDone () takeMVar testDone +-- | Verify that if a peer sends an address or data which exceeds the maximum+-- length, that peer's connection will be terminated, but other peers will+-- not be affected.+testMaxLength :: IO ()+testMaxLength = do++ Right serverTransport <- createTransport "127.0.0.1" "9998" ((,) "127.0.0.1") $ defaultTCPParameters {+ -- 17 bytes should fit every valid address at 127.0.0.1.+ -- Port is at most 5 bytes (65536) and id is a base-10 Word32 so+ -- at most 10 bytes. We'll have one client with a 5-byte port to push it+ -- over the chosen limit of 16+ tcpMaxAddressLength = 16+ , tcpMaxReceiveLength = 8+ }+ Right goodClientTransport <- createTransport "127.0.0.1" "9999" ((,) "127.0.0.1") defaultTCPParameters+ Right badClientTransport <- createTransport "127.0.0.1" "10000" ((,) "127.0.0.1") defaultTCPParameters++ serverAddress <- newEmptyMVar+ testDone <- newEmptyMVar+ goodClientConnected <- newEmptyMVar+ goodClientDone <- newEmptyMVar+ badClientDone <- newEmptyMVar++ forkTry $ do+ Right serverEp <- newEndPoint serverTransport+ putMVar serverAddress (address serverEp)+ readMVar badClientDone+ ConnectionOpened _ _ _ <- receive serverEp+ Received _ _ <- receive serverEp+ -- Will lose the connection when the good client sends 9 bytes.+ ErrorEvent (TransportError (EventConnectionLost _) _) <- receive serverEp+ readMVar goodClientDone+ putMVar testDone ()++ forkTry $ do+ Right badClientEp <- newEndPoint badClientTransport+ address <- readMVar serverAddress+ -- Wait until the good client connects, then try to connect. It'll fail,+ -- but the good client should still be OK.+ readMVar goodClientConnected+ Left (TransportError ConnectFailed _)+ <- connect badClientEp address ReliableOrdered defaultConnectHints+ closeEndPoint badClientEp+ putMVar badClientDone ()++ forkTry $ do+ Right goodClientEp <- newEndPoint goodClientTransport+ address <- readMVar serverAddress+ Right conn <- connect goodClientEp address ReliableOrdered defaultConnectHints+ putMVar goodClientConnected ()+ -- Wait until the bad client has tried and failed to connect before+ -- attempting a send, to ensure that its failure did not affect us.+ readMVar badClientDone+ Right () <- send conn ["00000000"]+ -- The send which breaches the limit does not appear to fail, but the+ -- (heavyweight) connection is now severed. We can reliably determine that+ -- by receiving.+ Right () <- send conn ["000000000"]+ ErrorEvent (TransportError (EventConnectionLost _) _) <- receive goodClientEp+ closeEndPoint goodClientEp+ putMVar goodClientDone ()++ readMVar testDone+ closeTransport badClientTransport+ closeTransport goodClientTransport+ closeTransport serverTransport++-- | Ensure that an end point closes up OK even if the peer disobeys the+-- protocol.+testCloseEndPoint :: IO ()+testCloseEndPoint = do++ serverAddress <- newEmptyMVar+ serverFinished <- newEmptyMVar++ -- A server which accepts one connection and then attempts to close the+ -- end point.+ forkTry $ do+ Right transport <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters+ Right ep <- newEndPoint transport+ putMVar serverAddress (address ep)+ ConnectionOpened _ _ _ <- receive ep+ Just () <- timeout 5000000 (closeEndPoint ep)+ putMVar serverFinished ()+ return ()++ -- A nefarious client which connects to the server then stops responding.+ forkTry $ do+ Just (hostName, serviceName, endPointId) <- decodeEndPointAddress <$> readMVar serverAddress+ addr:_ <- N.getAddrInfo (Just N.defaultHints) (Just hostName) (Just serviceName)+ sock <- N.socket (N.addrFamily addr) N.Stream N.defaultProtocol+ N.connect sock (N.addrAddress addr)+ let endPointAddress = "127.0.0.1:0:0"+ -- Version 0x00000000 handshake data.+ v0handshake = [+ encodeWord32 endPointId+ , encodeWord32 (fromIntegral (BS.length endPointAddress))+ , endPointAddress+ ]+ -- Version, and total length of the versioned handshake.+ handshake = [+ encodeWord32 0x00000000+ , encodeWord32 (fromIntegral (BS.length (BS.concat v0handshake)))+ ]+ sendMany sock $+ handshake+ ++ v0handshake+ ++ [ -- Create a lightweight connection.+ encodeWord32 (encodeControlHeader CreatedNewConnection)+ , encodeWord32 1024+ ]+ readMVar serverFinished+ N.close sock++ readMVar serverFinished++-- | Ensure that if the peer's claimed host doesn't match its actual host,+-- the connection is rejected (when tcpCheckPeerHost is enabled).+testCheckPeerHost :: IO ()+testCheckPeerHost = do++ let params = defaultTCPParameters { tcpCheckPeerHost = True }+ Right transport1 <- createTransport "127.0.0.1" "0" ((,) "127.0.0.1") params+ -- This transport claims 127.0.0.2 as its host, but connections from it to+ -- an EndPoint on transport1 will show 127.0.0.1 as the socket's source host.+ Right transport2 <- createTransport "127.0.0.1" "0" ((,) "127.0.0.2") defaultTCPParameters++ Right ep1 <- newEndPoint transport1+ Right ep2 <- newEndPoint transport2++ Left err <- connect ep2 (address ep1) ReliableOrdered defaultConnectHints++ TransportError ConnectFailed "setupRemoteEndPoint: Host mismatch 127.0.0.1"+ <- return err++ return ()+ main :: IO () main = do tcpResult <- tryIO $ runTests- [ ("Use random port" , testUseRandomPort)+ [ ("Use random port", testUseRandomPort) , ("EarlyDisconnect", testEarlyDisconnect) , ("EarlyCloseSocket", testEarlyCloseSocket) , ("IgnoreCloseSocket", testIgnoreCloseSocket)@@ -802,10 +1050,13 @@ , ("Reconnect", testReconnect) , ("UnidirectionalError", testUnidirectionalError) , ("InvalidCloseConnection", testInvalidCloseConnection)+ , ("MaxLength", testMaxLength)+ , ("CloseEndPoint", testCloseEndPoint)+ , ("CheckPeerHost", testCheckPeerHost) ] -- Run the generic tests even if the TCP specific tests failed.. testTransport (either (Left . show) (Right) <$>- createTransport "127.0.0.1" "0" defaultTCPParameters)+ createTransport "127.0.0.1" "0" ((,) "127.0.0.1") defaultTCPParameters) -- ..but if the generic tests pass, still fail if the specific tests did not case tcpResult of Left err -> throwIO err