packages feed

mariadb-haskell-0.1.0.0: validation/hs/Validation/WireProxy.hs

{-# LANGUAGE ScopedTypeVariables #-}
-- | Transparent byte counters for real compressed/plain server exchanges.
-- Neither TLS nor MariaDB framing is decoded or rewritten by this proxy.
module Validation.WireProxy (withWireProxy) where

import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.IORef
import qualified Data.ByteString as B
import qualified Network.Socket as N
import qualified Network.Socket.ByteString as NS
import System.Timeout
import Database.MySQL.Base (ConnectInfo(..))

-- | Counters are (bytes sent to MariaDB, bytes received from MariaDB). Updates
-- happen before forwarding, so a fully consumed reply has already been charged.
-- The proxy accepts one connection and joins every worker during cleanup.
withWireProxy :: ConnectInfo -> (ConnectInfo -> IO (Integer,Integer) -> IO a) -> IO a
withWireProxy ci action = bracket listener N.close $ \server -> do
    N.SockAddrInet port _ <- N.getSocketName server
    counters <- newIORef (0,0)
    finished <- newEmptyMVar
    let serve = bracket (fst <$> N.accept server) N.close $ \client -> do
            addresses <- N.getAddrInfo (Just N.defaultHints {N.addrSocketType=N.Stream})
                (Just (ciHost ci)) (Just (show (ciPort ci)))
            address <- case addresses of first:_ -> pure first; [] -> fail "proxy backend address unavailable"
            bracket (N.socket (N.addrFamily address) N.Stream N.defaultProtocol) N.close $ \backend -> do
                N.connect backend (N.addrAddress address)
                done <- newEmptyMVar
                let forward upstream from to = do
                        bytes <- NS.recv from 32768
                        unless (B.null bytes) $ do
                            atomicModifyIORef' counters $ \(sent,received) ->
                                let size = toInteger (B.length bytes)
                                in (if upstream then (sent+size,received) else (sent,received+size),())
                            NS.sendAll to bytes
                            forward upstream from to
                mask $ \restore -> do
                    firstDone <- newEmptyMVar
                    secondDone <- newEmptyMVar
                    first <- forkFinally (restore (forward True client backend))
                        (\_ -> putMVar firstDone () >> void (tryPutMVar done ()))
                    second <- forkFinally (restore (forward False backend client))
                        (\_ -> putMVar secondDone () >> void (tryPutMVar done ())) `onException` killThread first
                    let cleanup = do
                            killThread first
                            killThread second
                            joined <- timeout 1000000 (takeMVar firstDone >> takeMVar secondDone)
                            when (joined == Nothing) (fail "proxy forwarding workers did not finish")
                    restore (takeMVar done) `finally` cleanup
        stop worker = do
            killThread worker
            joined <- timeout 2000000 (takeMVar finished)
            when (joined == Nothing) (fail "proxy server did not finish")
    bracket (forkFinally serve (const (putMVar finished ()))) stop $ \_ ->
        action ci {ciHost="127.0.0.1",ciPort=port} (readIORef counters)
  where
    listener = bracketOnError (N.socket N.AF_INET N.Stream N.defaultProtocol) N.close $ \sock -> do
        N.setSocketOption sock N.ReuseAddr 1
        N.bind sock (N.SockAddrInet 0 (N.tupleToHostAddress (127,0,0,1)))
        N.listen sock 1
        pure sock