http2-tls-0.5.7: Network/HTTP2/TLS/IO.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Network.HTTP2.TLS.IO where
import Control.Concurrent.STM
import qualified Control.Exception as E
import Control.Monad (when)
import Data.ByteString (ByteString)
import Data.Either (isRight)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Network.Socket
import Network.Socket.BufferPool
import qualified Network.Socket.ByteString as NSB
import Network.TLS hiding (HostName)
import Network.HTTP2.Client (HTTP2Error (..))
import System.IO.Error (isEOFError)
import qualified System.IO.Error as E
import qualified System.TimeManager as T
import Network.HTTP2.TLS.Server.Settings
----------------------------------------------------------------
-- HTTP2: confReadN == recvTLS
-- TLS: recvData == contextRecv == backendRecv
----------------------------------------------------------------
-- | A receiving function which gives up when the 'STM' action fires
-- first. It waits in 'STM', so the timeout's kill reaches it on
-- Windows too.
mkRecvTCP :: Settings -> STM () -> Socket -> IO (IO ByteString)
mkRecvTCP settings giveUp sock = do
recvWith' <- mkRecvTCPWith settings sock
return $ recvWith' giveUp >>= orTimeout
-- | A receiving function which takes what it gives up on at each call.
mkRecvTCPWith :: Settings -> Socket -> IO (STM () -> IO (Either () ByteString))
mkRecvTCPWith Settings{..} sock = do
pool <- newBufferPool settingsReadBufferLowerLimit settingsReadBufferSize
return $ receiveWith sock pool
orTimeout :: Either () a -> IO a
orTimeout = either (\_ -> E.throwIO ConnectionIsTimeout) return
sendTCP :: Socket -> ByteString -> IO ()
sendTCP sock = NSB.sendAll sock
-- | 'sendTCP' which gives up when the 'STM' action fires first.
sendTCPWith :: Socket -> STM () -> ByteString -> IO (Either () ())
sendTCPWith sock giveUp bs = NSB.sendAllWith sock bs giveUp
----------------------------------------------------------------
-- | Sending and receiving functions.
-- Tiemout is reset when they return.
-- One exception is the slowloris attach prevention.
-- See 'settingsSlowlorisSize'.
data IOBackend = IOBackend
{ send :: ByteString -> IO ()
-- ^ Sending.
, sendWith :: STM () -> ByteString -> IO (Either () ())
-- ^ Sending which gives up when the 'STM' action fires first,
-- answering 'Left'. Part of the data may have gone by then, so the
-- connection is to be closed.
--
-- @since 0.5.7
, sendMany :: [ByteString] -> IO ()
-- ^ Sending many.
, recv :: IO ByteString
-- ^ Receiving.
, recvWith :: STM () -> IO (Either () ByteString)
-- ^ Receiving which gives up when the 'STM' action fires first,
-- answering 'Left'. Part of a TLS record may have been read by
-- then, so the connection is to be closed.
--
-- @since 0.5.7
, requestSock :: Socket
-- ^ The socket for the request
, mySockAddr :: SockAddr
, peerSockAddr :: SockAddr
}
timeoutIOBackend :: T.Handle -> Settings -> IOBackend -> IOBackend
timeoutIOBackend th Settings{..} IOBackend{..} =
IOBackend
{ send = send'
, sendWith = sendWith'
, sendMany = sendMany'
, recv = recv'
, recvWith = recvWith'
, ..
}
where
send' bs = send bs >> T.tickle th
sendWith' giveUp bs = do
r <- sendWith giveUp bs
when (isRight r) $ T.tickle th
return r
sendMany' bss = sendMany bss >> T.tickle th
recv' = do
bs <- recv
tickleIfLarge bs
return bs
recvWith' giveUp = do
r <- recvWith giveUp
either (\_ -> return ()) tickleIfLarge r
return r
tickleIfLarge bs = when (BS.length bs > settingsSlowlorisSize) $ T.tickle th
-- | The 'TVar's are the ones given to 'mkBackend': what a send and a
-- receive of the backend give up on.
tlsIOBackend :: TVar (STM ()) -> TVar (STM ()) -> Context -> Socket -> IO IOBackend
tlsIOBackend giveUpVar recvGiveUpVar ctx sock = do
mysa <- getSocketName sock
peersa <- getPeerName sock
return $
IOBackend
{ send = sendTLS ctx
, sendWith = \giveUp bs -> do
atomically $ writeTVar giveUpVar giveUp
(Right () <$ sendTLS ctx bs) `E.catch` gaveUp
, sendMany = sendManyTLS ctx
, recv = recvTLS ctx
, recvWith = \giveUp -> do
atomically $ writeTVar recvGiveUpVar giveUp
((Right <$> recvTLS ctx) `E.catch` gaveUp)
`E.finally` atomically (writeTVar recvGiveUpVar retry)
, requestSock = sock
, mySockAddr = mysa
, peerSockAddr = peersa
}
tcpIOBackend :: Settings -> Socket -> IO IOBackend
tcpIOBackend settings sock = do
recvWith' <- mkRecvTCPWith settings sock
mysa <- getSocketName sock
peersa <- getPeerName sock
return $
IOBackend
{ send = sendTCP sock
, sendWith = sendTCPWith sock
, sendMany = \_ -> return ()
, recv = recvWith' retry >>= orTimeout
, recvWith = recvWith'
, requestSock = sock
, mySockAddr = mysa
, peerSockAddr = peersa
}
----------------------------------------------------------------
sendTLS :: Context -> ByteString -> IO ()
sendTLS ctx = afterCloseNotify . sendData ctx . LBS.fromStrict
sendManyTLS :: Context -> [ByteString] -> IO ()
sendManyTLS ctx = afterCloseNotify . sendData ctx . LBS.fromChunks
-- | tls answers the peer's close_notify with its own, so a send after it
-- has nowhere to go: dropped, as the GOAWAY which closes the connection.
afterCloseNotify :: IO () -> IO ()
afterCloseNotify = E.handle $ \e -> case e of
PostHandshake Error_EOF -> return ()
_ -> E.throwIO e
-- TLS version of recv (decrypting) without a cache.
recvTLS :: Context -> IO ByteString
recvTLS ctx = E.handle onEOF $ recvData ctx
where
onEOF e
| Just (PostHandshake Error_EOF) <- E.fromException e = return ""
| Just ioe <- E.fromException e, isEOFError ioe = return ""
| otherwise = E.throwIO e
----------------------------------------------------------------
-- | A send of the backend gives up on what the first 'TVar' holds, which
-- the http2 library sets once it sends through 'sendWith', and a
-- receive on what the second holds, which 'recvWith' sets.
mkBackend :: Settings -> TVar (STM ()) -> TVar (STM ()) -> Socket -> IO Backend
mkBackend settings giveUpVar recvGiveUpVar sock = do
let send' bs = do
giveUp <- readTVarIO giveUpVar
sendTCPWith sock giveUp bs
>>= either (\_ -> E.throwIO ConnectionIsTimeout) return
recvWith' <- mkRecvTCPWith settings sock
let recv' = readTVarIO recvGiveUpVar >>= recvWith' >>= orTimeout
recvN <- makeRecvN "" recv'
return
Backend
{ backendFlush = return ()
, backendClose =
gracefulClose sock 5000 `E.catchIOError` \_ -> return ()
, backendSend = send'
, backendRecv = recvN
}
-- | close_notify, as a last word: a send which the http2 library has
-- given up on is not an error here.
byeGivingUp :: Context -> IO ()
byeGivingUp ctx = bye ctx `E.catch` \e -> () <$ gaveUp e
gaveUp :: HTTP2Error -> IO (Either () a)
gaveUp ConnectionIsTimeout = return $ Left ()
gaveUp e = E.throwIO e