packages feed

second-transfer-0.10.0.2: tests/tests-hs-src/SecondTransfer/Test/DecoySession.hs

{-# LANGUAGE TemplateHaskell, OverloadedStrings #-}

module SecondTransfer.Test.DecoySession where



import           Control.Concurrent
import           Control.Concurrent.STM.TMVar
import           Control.Concurrent.STM.TChan
import           Control.Exception
import           Control.Lens                     (view, (^.), over)
import qualified Control.Lens                     as L
import           Control.Monad.STM                (atomically)

import qualified Network.HTTP2                    as NH2
import qualified Network.HPACK                    as HP

import qualified Data.ByteString                  as B
import qualified Data.ByteString.Lazy             as LB
import qualified Data.ByteString.Builder          as Bu
import qualified Data.Sequence                    as Sq
import           Data.Sequence                    ( (<|), (|>), ViewL(..), Seq )

import           SecondTransfer.Http2
import           SecondTransfer.Types
import           SecondTransfer.Http2.Session
import           SecondTransfer.Sessions.Config
import           SecondTransfer.Sessions.Internal
import           SecondTransfer.Http2.Framer
import           SecondTransfer.MainLoop.Internal
import           Debug.Trace

-- "Internal imports"
import           SecondTransfer.MainLoop.CoherentWorker (defaultEffects)


data DataContinuityEngine = DataContinuityEngine {
    _incoming_DCE :: Seq B.ByteString
    }


L.makeLenses ''DataContinuityEngine


emptyDCE :: DataContinuityEngine
emptyDCE =  DataContinuityEngine  Sq.empty


dceAddData :: DataContinuityEngine -> B.ByteString -> DataContinuityEngine
dceAddData dce moredata | B.length moredata > 0 = over incoming_DCE ( |>  moredata ) dce
dceAddData dce moredata | otherwise             = dce


tryGetThisManyData :: DataContinuityEngine -> Int -> Maybe (DataContinuityEngine, B.ByteString)
tryGetThisManyData dce n = let
    go_result = go n "" (dce ^. incoming_DCE)
    go :: Int -> Bu.Builder -> Seq B.ByteString -> Maybe (Seq B.ByteString, Bu.Builder)
    go n bld seq  | Sq.EmptyL <- Sq.viewl seq, n > 0   = Nothing
                  | Sq.EmptyL <- Sq.viewl seq, n == 0  = Just (seq, bld)
                  | lead :< rest <- Sq.viewl seq, n > B.length lead    = go (n - B.length lead) (bld `mappend` Bu.byteString lead) rest
                  | lead :< rest <- Sq.viewl seq, n == B.length lead   = Just ( rest, bld `mappend` Bu.byteString lead)
                  | lead :< rest <- Sq.viewl seq, n < B.length lead    = let
                      (tk, lv) = B.splitAt n lead
                      in Just ( lv <| rest, bld `mappend`  Bu.byteString tk  )
  in case go_result of
    Just (new_dce, bld ) -> Just
        (DataContinuityEngine new_dce,  LB.toStrict . Bu.toLazyByteString $ bld )

    Nothing -> Nothing


dceIsEmpty :: DataContinuityEngine -> Bool
dceIsEmpty dce = Sq.null $ dce ^. incoming_DCE


dceGetAllData :: DataContinuityEngine -> (DataContinuityEngine, B.ByteString)
dceGetAllData dce = (emptyDCE, all_together)
  where
    all_together = LB.toStrict . Bu.toLazyByteString .  L.foldMapOf (incoming_DCE . L.folded) Bu.byteString $ dce


data DecoySession = DecoySession {
    _inputDataChannel     :: TChan B.ByteString
    ,_outputDataChannel   :: TChan B.ByteString
    ,_sessionThread       :: ThreadId
    ,_remainingOutputBit  :: TMVar B.ByteString
    ,_nh2Settings         :: NH2.Settings
    ,_sessionThrowed      :: TMVar Bool
    ,_waiting             :: TMVar ThreadId
    ,_encodeHeadersHere   :: TMVar HP.DynamicTable
    ,_decodeHeadersHere   :: TMVar HP.DynamicTable

    ,_incomingData        :: TMVar DataContinuityEngine
    ,_clientState         :: ClientState
    -- Execute this when you want to use a powerfull client session
    ,_startClientSessionCallback :: IO ClientState
    }


L.makeLenses ''DecoySession



-- errorsSessionConfig :: MVar Bool -> SessionsConfig
-- errorsSessionConfig mvar = set (sessionsCallbacks . reportErrorCallback_SC)
--      (Just $ setError mvar) defaultSessionsConfig


-- | Creates a IOCallbacks where "pullAction" reads from input_data_channel and where
--   "pushData" writes to output_data_channel.
channelsToIOCallbacks :: TChan B.ByteString -> TChan B.ByteString -> TMVar DataContinuityEngine -> IOCallbacks
channelsToIOCallbacks input_data_channel output_data_channel incoming_data_tmvar =
  let
    push_action  :: PushAction
    push_action lazy_bs  =  atomically $
        writeTChan output_data_channel $ LB.toStrict lazy_bs

    pull_action  :: PullAction
    pull_action byte_count = atomically $ do
        incoming_data <- takeTMVar incoming_data_tmvar
        let
            go dce = let
                maybe_stuff = tryGetThisManyData dce byte_count
                in case maybe_stuff of
                    Nothing -> do
                        new_data <- readTChan input_data_channel
                        let
                            new_dce = dceAddData dce new_data
                        go new_dce

                    Just (new_bce, bs) -> do
                        putTMVar incoming_data_tmvar new_bce
                        return bs
        go incoming_data

    best_effort_pull_action :: BestEffortPullAction
    best_effort_pull_action can_block = atomically $ do
        incoming_data <- takeTMVar incoming_data_tmvar
        if dceIsEmpty incoming_data
          then
            if can_block
              then do
                -- Wait for the next item
                new_data <- readTChan input_data_channel
                -- Leave the empty thing alone
                putTMVar incoming_data_tmvar incoming_data
                return new_data
              else do
                putTMVar incoming_data_tmvar incoming_data
                return ""
          else do
              let
                  (new_dce, all_together) = dceGetAllData incoming_data
              putTMVar incoming_data_tmvar new_dce
              return all_together

    close_action :: CloseAction
    close_action =  return ()
  in
    IOCallbacks {
       _pushAction_IOC = push_action,
       _pullAction_IOC = pull_action,
       _closeAction_IOC = close_action,
       _bestEffortPullAction_IOC = best_effort_pull_action
       }


-- Supposed to be an HTTP/2 attendant
createDecoySession :: Attendant -> IO DecoySession
createDecoySession attendant = do
    input_data_channel  <- newTChanIO
    output_data_channel <- newTChanIO
    incoming_data_tmvar  <- newTMVarIO emptyDCE
    reverse_data_tmvar <- newTMVarIO emptyDCE
    session_throwed_tmvar <- newTMVarIO False
    client_state <- makeClientState
    client_sessions_context <- makeSessionsContext defaultSessionsConfig
    let
        attendant_callbacks = channelsToIOCallbacks input_data_channel output_data_channel incoming_data_tmvar
        client_callbacks = channelsToIOCallbacks output_data_channel input_data_channel reverse_data_tmvar
        start_client_session_callback :: IO ClientState
        start_client_session_callback = do
            -- Connection info is not used anyway with client cases
            wrapSession (ClientState_SP client_state) client_sessions_context (error "no-connection_data")  client_callbacks
            return client_state


    dtable_for_encoding <- HP.newDynamicTableForEncoding 4096
    dtable_for_encoding_mvar <- newTMVarIO dtable_for_encoding
    dtable_for_decoding <- HP.newDynamicTableForDecoding 4096
    dtable_for_decoding_mvar <- newTMVarIO dtable_for_decoding

    waiting_mvar <- newEmptyTMVarIO

    thread_id <- forkIO $ catch
        (do
            attendant nullConnectionData  attendant_callbacks
        )
        ((\ e -> do
            putStrLn $ "Exception: " ++ (show e)
            maybe_waiting <- atomically $ tryReadTMVar waiting_mvar
            case maybe_waiting of
                Just thread_id -> throwTo thread_id e
                Nothing -> return ()
        ):: SomeException -> IO ())

    remaining_output_bit <- newTMVarIO ""
    let session = DecoySession {
        _inputDataChannel  = input_data_channel,
        _outputDataChannel = output_data_channel,
        _sessionThread     = thread_id,
        _remainingOutputBit= remaining_output_bit,
        _nh2Settings       = NH2.defaultSettings,
        _waiting           = waiting_mvar,
        _encodeHeadersHere = dtable_for_encoding_mvar,
        _decodeHeadersHere = dtable_for_decoding_mvar,
        _incomingData      = incoming_data_tmvar,
        _sessionThrowed    = session_throwed_tmvar,
        _clientState       = client_state,
        _startClientSessionCallback = start_client_session_callback
        }
    return session


-- Tell when we are done with a decision
sessionIsUndone :: DecoySession -> IO Bool
sessionIsUndone session = error "NotImplemented"


-- Send a frame to a session
sendFrameToSession :: DecoySession -> (NH2.EncodeInfo, NH2.FramePayload) -> IO ()
sendFrameToSession session (encode_info, frame_payload) = do
    let
        bs_list = NH2.encodeFrameChunks encode_info frame_payload
        input_data_channel = session ^. inputDataChannel

    atomically $
        mapM_
            (writeTChan input_data_channel) bs_list


-- Read a frame from a session... if possible. It will block
-- until the frame comes out, but should fail if there is
-- an exception and/or the session is closed.
recvFrameFromSession :: DecoySession ->  IO (Maybe NH2.Frame)
recvFrameFromSession decoy_session = do
    let
        output_data_channel = decoy_session ^. outputDataChannel
        remaining_output_bit_mvar = decoy_session ^. remainingOutputBit
        pull_action =  readTChan output_data_channel
        settings = decoy_session ^. nh2Settings
        waiting_for_read = decoy_session ^. waiting
    my_thread_id <- myThreadId
    packet <- atomically $ do
        {-# SCC aT #-} putTMVar waiting_for_read my_thread_id
        remaining_output_bit <- {-# SCC bT #-}  takeTMVar remaining_output_bit_mvar
        (packet, rest) <-  {-# SCC cT #-}  readNextChunkAndContinue http2FrameLength remaining_output_bit pull_action
        {-# SCC dT #-} putTMVar remaining_output_bit_mvar rest
        {-# SCC eT #-} takeTMVar waiting_for_read
        return packet
    let
        error_or_frame = NH2.decodeFrame settings packet
    case error_or_frame of
        Left   _      -> return Nothing
        Right  frame  -> return $ Just frame

-- Reads all frames from a session and assembles a response. Notice that this is a buggy hack,
-- because continuation frames and such are not considered.
-- T
-- readResponseFromSession :: DecoySession -> NH2.StreamId -> IO (Headers, B.ByteString, [NH2.Frame])



-- Encode headers to send to the session
-- TODO: There is an important bug here... we are using the default encoding
-- strategy everywhere
encodeHeadersForSession :: DecoySession -> Headers -> IO B.ByteString
encodeHeadersForSession decoy_session headers =
    do
        let
            mv = decoy_session ^. encodeHeadersHere
        dtable <- atomically $ takeTMVar mv
        (dtable', bs ) <- HP.encodeHeader HP.defaultEncodeStrategy dtable headers
        atomically $ putTMVar mv dtable'
        return bs

-- Decode headers to receive them from the session
decodeHeadersForSession :: DecoySession -> B.ByteString -> IO Headers
decodeHeadersForSession decoy_session bs =  do
    let
        mv = decoy_session ^. decodeHeadersHere
    dtable <- atomically $ takeTMVar mv
    (dtable', headers) <- HP.decodeHeader dtable bs
    atomically $ putTMVar mv dtable'
    return headers

-- Send raw data to a session
sendRawDataToSession :: DecoySession -> B.ByteString -> IO ()
sendRawDataToSession decoy_session data_to_send = do
    let
        input_data_channel = decoy_session ^. inputDataChannel
        waiting_for_write = decoy_session ^. waiting

    thread_id <- myThreadId
    atomically $ do
        putTMVar waiting_for_write thread_id
        writeTChan input_data_channel data_to_send
        takeTMVar waiting_for_write
    return ()


-- Send a headers frame to the session that ends the stream...
performRequestSimple :: DecoySession -> Int -> Headers -> IO ()
performRequestSimple decoy_session stream_id headers = do
    headers_data <- encodeHeadersForSession decoy_session headers
    let
        frame = NH2.HeadersFrame
            Nothing  -- No priority
            headers_data
        frame_data_ei = NH2.EncodeInfo {
            NH2.encodeFlags = NH2.setEndStream . NH2.setEndHeader $ NH2.defaultFlags,
            NH2.encodeStreamId = stream_id,
            NH2.encodePadding = Nothing
            }

    sendFrameToSession decoy_session (frame_data_ei,frame)


-- Read raw data from a session. Normally blocks until data be available.
-- It returns Nothing when the session is to be considered finished.
recvRawDataFromSession :: DecoySession -> IO (Maybe B.ByteString)
recvRawDataFromSession decoy_session = error "NotImplemented"