packages feed

htalkat-0.1.2: RelayStream.hs

-- This file is part of htalkat
-- Copyright (C) 2021 Martin Bays <mbays@sdf.org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of version 3 of the GNU General Public License as
-- published by the Free Software Foundation, or any later version.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see http://www.gnu.org/licenses/.

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module RelayStream where

import           Control.Concurrent
import           Control.Exception              (SomeException, handle)
import           Control.Monad                  (foldM_, forever, unless, void,
                                                 when)
import           System.Timeout                 (timeout)

import qualified Data.ByteString                as BS
import qualified Data.ByteString.Lazy           as BL
import qualified Data.Text.Encoding.Error       as T
import qualified Data.Text.Lazy                 as T
import qualified Data.Text.Lazy.Encoding        as T
import qualified Network.Socket                 as S
import qualified Network.Socket.ByteString.Lazy as SL
import qualified Network.TLS                    as TLS
import qualified Time.System                    as TM
import qualified Time.Types                     as TM

import           Mundanities
import           TimedText

data WriteOrder = WriteFirst | WriteSecond
    deriving (Eq,Ord,Show)

relayStream :: TLS.Context -> WriteOrder -> S.Socket -> IO ()
relayStream ctxt ord dSock = do
    receivedHandshake <- newEmptyMVar
    finished <- newEmptyMVar
    rawInChan <- newChan
    let abort = putMVar finished ()
        abortOnErr = handle abortHandler where
            abortHandler :: Monoid a => SomeException -> IO a
            abortHandler _ = abort >> pure mempty
        recvAll = do
            b <- TLS.recvData ctxt
            case BS.uncons b of
                Nothing -> abort
                Just (h,_) -> do
                    ok <- tryReadMVar receivedHandshake >>= \case
                        Just ok -> pure ok
                        Nothing -> do
                            let isHandshakeByte = h == introByte
                            putMVar receivedHandshake isHandshakeByte
                            if isHandshakeByte
                                then pure True
                                else abort >> pure False
                    if ok then writeChan rawInChan b >> recvAll
                        else writeChan rawInChan BS.empty
        sendHandshake = do
            when (ord == WriteSecond) . void $ readMVar receivedHandshake
            TLS.sendData ctxt $ BL.singleton introByte

    sockMV <- newEmptyMVar
    sockThread <- forkIO $ putMVar sockMV . fst =<< S.accept dSock

    _ <- forkIO $ do
        sock <- readMVar sockMV
        abortOnErr sendHandshake
        tsOutChan <- newChan
        rawOutChan <- newChan
        _ <- forkIO . abortOnErr $ do
            writeList2Chan rawOutChan . T.unpack . T.decodeUtf8With T.lenientDecode =<<
                SL.getContents sock
            abort
        pausesThread <- forkIO $ insertPauses rawOutChan tsOutChan
        abortOnErr $ sendAll tsOutChan
        killThread pausesThread

    _ <- forkIO $ do
        tsInChan <- newChan
        decodeTTThread <- forkIO $
            writeList2Chan tsInChan . decodeTimedText . BL.fromChunks =<< getChanContents rawInChan
        _ <- forkIO . abortOnErr $ relayTimed tsInChan =<< readMVar sockMV
        abortOnErr recvAll
        killThread decodeTTThread

    _ <- takeMVar finished
    ignoreIOErr $ TLS.bye ctxt >> killThread sockThread
    tryTakeMVar sockMV >>= \case
        Nothing   -> pure ()
        Just sock -> S.gracefulClose sock 1000
    where
    introByte = fromIntegral $ fromEnum 'T'

    insertPauses rawChan ttChan = TM.timeCurrentP >>= insertPauses'
        where
        insertPauses' e = do
            c <- readChan rawChan
            e' <- TM.timeCurrentP
            let ms = elapsedPToMS $ e' - e
            when (ms > 0) . writeChan ttChan . Left $ fromIntegral ms
            writeChan ttChan $ Right c
            insertPauses' e'

    sendAll ttChan = forever $ do
        readBufMV <- newMVar []
        _ <- timeout sendTimeout . forever $
            modifyMVar_ readBufMV . (pure .) . (:) =<< readChan ttChan
        rtt <- readMVar readBufMV
        unless (null rtt) . TLS.sendData ctxt . rechunk . encodeTimedText $ reverse rtt
        where
        rechunk =
            -- TLS.sendData sends one packet per chunk, while encodeTimedText
            -- returns a chunk per char, so it's important to rechunk.
            BL.fromStrict . BL.toStrict
        sendTimeout = 1000 * 300

    relayTimed chan sock = foldM_ sendTimed' Nothing =<< getChanContents chan where
        sendTimed' :: Maybe TM.ElapsedP -> Either Int Char -> IO (Maybe TM.ElapsedP)
        sendTimed' Nothing (Right c) = do
            threadDelay bufferTime
            e <- TM.timeCurrentP
            sendTimed' (Just e) (Right c)
        sendTimed' Nothing _ = pure Nothing
        sendTimed' (Just e) (Right c) = do
            SL.sendAll sock . T.encodeUtf8 $ T.singleton c
            pure $ Just e
        sendTimed' (Just e) (Left n) = do
            delayed <- elapsedPToMS . flip (-) e <$> TM.timeCurrentP
            when (n > delayed) . threadDelay . (1000 *) $ n - delayed
            pure $ if n == pauseMax && n < delayed
                then Nothing
                else Just $ e + msToElapsedP n
        bufferTime = 1000 * 300

    msToElapsedP :: Int -> TM.ElapsedP
    msToElapsedP ms | (s,ms') <- fromIntegral ms `divMod` 1000 =
        TM.ElapsedP (TM.Elapsed (TM.Seconds s)) (TM.NanoSeconds $ 1000000 * ms')
    elapsedPToMS :: TM.ElapsedP -> Int
    elapsedPToMS (TM.ElapsedP (TM.Elapsed (TM.Seconds s)) (TM.NanoSeconds ns)) =
        fromIntegral $ s*1000 + ns `div` 1000000