packages feed

danecheck-1.0.0.0: Dane/Scanner/SMTP/Internal.hs

module Dane.Scanner.SMTP.Internal
  ( ProtoState(..)
  , SmtpM
  , SmtpState(..)
  , SmtpReply(..)
  , SmtpErr(..)
  , SmtpConn(..)
  , SmtpFeature(..)
  , timeLimit
  , timeLeft
  , startState
  , ioErr
  , eofErr
  , timeErr
  , errLoc
  , tryIO
  ) where

import qualified System.Clock as Sys
import           System.IO.Error as Sys

import           GHC.IO.Exception (IOErrorType(EOF, TimeExpired))

import           Control.Exception (SomeException, IOException, try)
import           Control.Monad.IO.Class (liftIO)
import           Control.Monad.Trans.State.Strict (StateT, gets)
import           Data.ByteString.Char8 (ByteString, pack)
import           Data.IORef (IORef, newIORef)
import           Data.Int (Int64)
import           Network.Socket (Socket)
import qualified Network.TLS as TLS

import           Dane.Scanner.Opts (SigAlgs)
import           Dane.Scanner.SMTP.Certs (ChainInfo)

data ProtoState = ProtoState
  { smtpState   :: !SmtpState
  , smtpErr     :: !SmtpErr
  , clientName  :: !ByteString
  , serverName  :: !String
  , smtpConn    :: !SmtpConn
  , smtpTimeout :: !Int
  , llenLimit   :: !Int
  , ioDeadline  :: !Sys.TimeSpec
  , features    :: ![SmtpFeature]
  , chainRef    :: !(IORef ChainInfo)
  , tlsSigAlgs  :: !SigAlgs    -- ^ TLS signature-algorithm preference
  }

type SmtpM = StateT ProtoState IO

data SmtpConn = SmtpPlain Socket
              | SmtpTLS TLS.Context

data SmtpFeature = FeatureTLS
                 | FeatureSIZE Int64
                 | FeatureUTF8
  deriving (Eq)

data SmtpState = CONNECT
               | GREETING
               | EHLO
               | STARTTLS
               | DOTLS
               | QUIT
               | DONE
  deriving (Eq, Enum, Show, Ord)

data SmtpReply = SmtpReply
  { replyCode :: !Int
  , replyCont :: !Bool
  , replyText :: !ByteString
  }
  deriving (Show)

data SmtpErr = SmtpOK
             | TlsHandError TLS.TLSError
             | TlsRecvError
             | TlsSendError
             | DataErr IOException
             | ProtoErr Int ByteString
             | OtherErr SomeException

timeLimit :: Int -> IO Sys.TimeSpec
timeLimit tmout = do
  now <- Sys.getTime Sys.Monotonic
  return $! Sys.fromNanoSecs
         $ (fromIntegral tmout * 1000) + Sys.toNanoSecs now

timeLeft :: SmtpM Int
timeLeft = do
  deadline <- gets ioDeadline
  now <- liftIO $ Sys.getTime Sys.Monotonic
  return $! fromIntegral
         $ flip div 1000
         $ Sys.toNanoSecs
         $ Sys.diffTimeSpec deadline now

-- | Initialize the client SMTP protocol state
--
startState :: SigAlgs    -- ^ TLS signature-algorithm preference
           -> String     -- ^ SMTP client EHLO name
           -> String     -- ^ SMTP server name
           -> Int        -- ^ SMTP command timeout (us)
           -> Int        -- ^ SMTP response line length limit
           -> Socket     -- ^ Socket connected to the SMTP server
           -> IO ProtoState
startState sigAlgs helo peer tout llen sock = do
  cref <- newIORef undefined
  deadline <- timeLimit tout
  return ProtoState
    { smtpState = GREETING
    , clientName = pack helo
    , serverName = peer
    , smtpConn   = SmtpPlain sock
    , smtpTimeout = tout
    , ioDeadline = deadline
    , smtpErr    = SmtpOK
    , llenLimit  = llen
    , features   = []
    , chainRef   = cref
    , tlsSigAlgs = sigAlgs
    }

ioErr :: String -> IOException -> IOException
ioErr loc err = Sys.ioeSetLocation err loc

eofErr :: String -> IOException
eofErr loc = Sys.mkIOError EOF loc Nothing Nothing

timeErr :: String -> IOException
timeErr loc = Sys.mkIOError TimeExpired loc Nothing Nothing

errLoc :: IOException -> ProtoState -> IOException
errLoc err st =
  let loc = Sys.ioeGetLocation err
  in if (loc /= "")
  then Sys.ioeSetLocation err $
    (showString. show $ smtpState st).
    showChar ' '.
    showString loc $ ""
  else Sys.ioeSetLocation err $ show $ smtpState st

-- | 'Control.Exception.try' monomorphised at 'IOException', matching
-- the @safe-exceptions@ helper of the same name.  Async exceptions
-- (anything wrapped in 'SomeAsyncException') pass through unchanged
-- — only @IOException@s are caught — which is what every caller
-- here wants: the @timeout@ wrapping each call needs async
-- @TimeExpired@ to propagate so the timeout actually fires.
tryIO :: IO a -> IO (Either IOException a)
tryIO = try