second-transfer-0.10.0.1: hs-src/SecondTransfer/IOCallbacks/Types.hs
{-# LANGUAGE ExistentialQuantification, TemplateHaskell, DeriveDataTypeable #-}
module SecondTransfer.IOCallbacks.Types (
-- | Functions for passing data to external parties
-- The callbacks here should have a blocking behavior and not
-- return empty results unless at end of file.
-- * Fundamental types and accessors
PushAction
, PullAction
, BestEffortPullAction
, Attendant
, CloseAction
, IOCallbacks (..)
, pushAction_IOC
, pullAction_IOC
, closeAction_IOC
, bestEffortPullAction_IOC
-- * Classifying IO callbacks
-- $ classifiers
, IOChannels (..)
, PlainTextIO
, TLSEncryptedIO
, TLSServerIO
, TLSClientIO
, SOCKS5Preface
, ConnectionData (..)
, addr_CnD
-- * Utility functions
, PullActionWrapping
, newPullActionWrapping
, pullFromWrapping'
, bestEffortPullFromWrapping
) where
import Control.Lens
import Data.IORef
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.Builder as Bu
import SecondTransfer.Sessions.HashableSockAddr (HashableSockAddr)
-- | Callback type to push data to a channel. Part of this
-- interface is the abstract exception type IOProblem. Throw an
-- instance of it from here to notify the session that the connection has
-- been broken. There is no way to signal "normal termination", since
-- HTTP/2's normal termination can be observed at a higher level when a
-- GO_AWAY frame is seen.
type PushAction = LB.ByteString -> IO ()
-- | Callback type to pull data from a channel in a best-effort basis.
-- When the first argument is True, the data-providing backend can
-- block if the input buffers are empty and await for new data.
-- Otherwise, it will return immediately with an empty ByteString
type BestEffortPullAction = Bool -> IO B.ByteString
-- | Callback type to pull data from a channel. The same
-- as to PushAction applies to exceptions thrown from
-- there. The first argument is the number of bytes to
-- pull from the medium. Barring exceptions, we always
-- know how many bytes we are expecting with HTTP/2.
type PullAction = Int -> IO B.ByteString
-- | Generic implementation of PullAction from BestEffortPullAction, where we keep around
-- any leftovers data ...
newtype PullActionWrapping = PullActionWrapping (IORef (Bu.Builder,Int), BestEffortPullAction)
-- The type above contains stuff already read, its length and the the action
newPullActionWrapping :: BestEffortPullAction -> IO PullActionWrapping
newPullActionWrapping best_effort_pull_action = do
bu_ref <- newIORef (mempty, 0)
return $ PullActionWrapping (bu_ref, best_effort_pull_action)
-- | The type of this function is also PullActionWrapping -> PullAction
-- There should be only one reader concurrently.
pullFromWrapping' :: PullActionWrapping -> Int -> IO B.ByteString
pullFromWrapping' (PullActionWrapping (x, bepa)) n = do
(hathz, len) <- readIORef x
let
nn = fromIntegral n
pullData hathz' len' =
if n <= len'
then do
let
hathz_lb = Bu.toLazyByteString hathz'
to_return = LB.toStrict . LB.take nn $ hathz_lb
new_hazth = Bu.lazyByteString . LB.drop nn $ hathz_lb
return (to_return, new_hazth, len' - n)
else do
-- Need to read more
more <- bepa True
-- Append
let
new_len = fromIntegral len' + B.length more
new_hazth = hathz' `mappend` Bu.byteString more
pullData new_hazth new_len
(to_return, new_hazth, new_len) <- pullData hathz len
writeIORef x (new_hazth, new_len)
return to_return
bestEffortPullFromWrapping :: PullActionWrapping -> Bool -> IO B.ByteString
bestEffortPullFromWrapping (PullActionWrapping (x, bepa)) False = do
(hathz, len) <- atomicModifyIORef' x $ \ (h,l) -> ((mempty,0), (h,l))
if len > 0
then do
return . LB.toStrict . Bu.toLazyByteString $ hathz
else do
-- At least we ought to try
bepa False
bestEffortPullFromWrapping (PullActionWrapping (x, bepa)) True = do
(hathz, len) <- atomicModifyIORef' x $ \ (h,l) -> ((mempty,0), (h,l))
if len > 0
then do
return . LB.toStrict . Bu.toLazyByteString $ hathz
else do
bepa True
-- | Callback that the session calls to realease resources
-- associated with the channels. Take into account that your
-- callback should be able to deal with non-clean shutdowns
-- also, for example, if the connection to the remote peer
-- is severed suddenly.
type CloseAction = IO ()
-- | A set of functions describing how to do I/O in a session.
-- There is one rule for IOCallbacks: only one user of it.
-- That is, only one can write, only one can read concurrently.
-- We don't protect for anything else, so concurrent read and
-- writes would result in terrible things happening.
data IOCallbacks = IOCallbacks {
-- | put some data in the channel
_pushAction_IOC :: PushAction,
-- | get exactly this much data from the channel. This function can
-- be used by HTTP/2 since lengths are pretty well built inside the
-- protocoll itself. An exception of type NoMoreDataException can be
-- raised from here inside if the channel is closed. This is done to
-- notify the caller that there is no more data. The alternative
-- would be to return an empty string, but that looks more hazardous
-- for the caller.
_pullAction_IOC :: PullAction,
-- | pull data from the channel, as much as the TCP stack wants to provide.
-- we have no option but use this one when talking HTTP/1.1, where the best
-- way to know the length is to scan until a Content-Length is found.
-- This will also raise NoMoreDataException when there is no more data.
-- Notice that with argument False, this may return an empty string.
_bestEffortPullAction_IOC :: BestEffortPullAction,
-- | this is called when we wish to close the channel.
_closeAction_IOC :: CloseAction
}
makeLenses ''IOCallbacks
-- $classifiers
-- Sometimes we need to classify the IO callbacks according to the operations
-- that they support, so we also create the following classes.
-- | An object a which is IOChannels
class IOChannels a where
-- | This method should only be invoked once for the a instance.
-- It will block/wait for any handshakes to complete, and only then
-- return a usable set of callbacks.
handshake :: a -> IO IOCallbacks
-- | Data exchanged through this channel is plain text
class IOChannels a => PlainTextIO a
-- | Data exchanged through this channel is the data of a TLS session
class IOChannels a => TLSEncryptedIO a
-- | Data exchanged through this channel begins with a SOCKS5 negotiation
class IOChannels a => SOCKS5Preface a
-- | The agent putting and retrieving data in this side of the channel should
-- behave as a TLS server
class TLSEncryptedIO a => TLSServerIO a
-- | The agent putting and retrieving data in this side of the channel should
-- behave as a TLS client
class TLSEncryptedIO a => TLSClientIO a
-- | PlainText wrapper
newtype PlainText = PlainText IOCallbacks
instance IOChannels PlainText where
handshake (PlainText io) = return io
instance PlainTextIO PlainText
newtype TLSServer = TLSServer IOCallbacks
instance IOChannels TLSServer where
handshake (TLSServer io) = return io
instance TLSEncryptedIO TLSServer
instance TLSServerIO TLSServer
-- | Some context related to a connection
newtype ConnectionData = ConnectionData { _addr_CnD :: Maybe HashableSockAddr }
makeLenses ''ConnectionData
-- | An Attendant is an entity that can speak a protocol, given
-- the presented I/O callbacks. It's work is to spawn a set
-- of threads to handle a client's session, and then return to
-- the caller. It shouldn'r busy the calling thread.
type Attendant = ConnectionData -> IOCallbacks -> IO ()