{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Network.Telnet.LibTelnet
( telnetInit
, OptionSpec(..)
, T.Flag
, T.flagProxy
, Telnet
, TelnetPtr
, HasTelnetPtr(..)
, EventHandler
, Event(..)
, Err(..)
, IsInfo(..)
, Var(..)
, telnetRecv
, telnetSend
, telnetIac
, telnetNegotiate
, telnetSubnegotiation
, telnetBeginCompress2
, telnetNewEnvironSend
, telnetNewEnviron
, telnetTTypeSend
, telnetTTypeIs
, telnetSendZmp
, telnetSendMssp
, T.TelnetException(..)
) where
import qualified Network.Telnet.LibTelnet.Ffi as F
import qualified Network.Telnet.LibTelnet.Iac as I
import Network.Telnet.LibTelnet.Options (Option(..), optMSSP)
import qualified Network.Telnet.LibTelnet.Types as T
import Control.Exception (throw, throwIO)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as B8
import Data.Foldable (traverse_)
import Data.Function (on)
import Data.List (groupBy)
import Data.Monoid ((<>))
import Data.Typeable (Typeable)
import Foreign (ForeignPtr, Ptr, peek, peekArray, withForeignPtr)
import Foreign.C.String (castCUCharToChar)
import GHC.Generics (Generic)
telnetInit :: [OptionSpec] -> [T.Flag] -> EventHandler -> IO Telnet
telnetInit options flags handler =
F.telnetInit options' (convertEventHandler handler) flags
where
options' = map f options
f (OptionSpec opt us him) =
let us' = if us then I.iacWill else I.iacWont
him' = if him then I.iacDo else I.iacDont
in T.TelnetTeloptT (fromIntegral $ unOption opt) us' him'
data OptionSpec = OptionSpec
{ _code :: Option
, _us :: Bool
, _him :: Bool
} deriving (Eq, Generic, Show, Typeable)
type Telnet = ForeignPtr T.TelnetT
type TelnetPtr = Ptr T.TelnetT
class HasTelnetPtr t where
withTelnetPtr :: t -> (TelnetPtr -> IO a) -> IO a
instance HasTelnetPtr Telnet where
withTelnetPtr = withForeignPtr
instance HasTelnetPtr TelnetPtr where
withTelnetPtr t f = f t
type EventHandler = TelnetPtr -> Event -> IO ()
data Event
= Received ByteString
| Send ByteString
| Warning Err
| Error Err
| Iac I.Iac
| Will Option
| Wont Option
| Do Option
| Dont Option
| Subnegotiation Option ByteString
| Zmp [ByteString]
| TerminalTypeSend
| TerminalTypeIs ByteString
| Compress Bool
| EnvironSend [(Var, ByteString)]
| Environ IsInfo [(Var, ByteString, ByteString)]
| Mssp [(ByteString, [ByteString])]
deriving (Eq, Generic, Show, Typeable)
data Err = Err
{ _file :: ByteString
, _func :: ByteString
, _msg :: ByteString
, _line :: Int
, _errcode :: T.TelnetErrorT
}
deriving (Eq, Generic, Show, Typeable)
data IsInfo = Is | Info deriving (Eq, Show)
data Var = Var | UserVar deriving (Eq, Show)
telnetRecv :: HasTelnetPtr t => t -> ByteString -> IO ()
telnetRecv t bs = withTelnetPtr t $ \telnetP -> F.telnetRecv telnetP bs
telnetSend :: HasTelnetPtr t => t -> ByteString -> IO ()
telnetSend t bs = withTelnetPtr t $ \telnetP -> F.telnetSend telnetP bs
telnetIac :: HasTelnetPtr t => t -> I.Iac -> IO ()
telnetIac t c = withTelnetPtr t $ \telnetP -> F.cTelnetIac telnetP c
telnetNegotiate :: HasTelnetPtr t => t -> I.Iac -> Option -> IO ()
telnetNegotiate t cmd opt = withTelnetPtr t $
\telnetP -> F.cTelnetNegotiate telnetP cmd opt
telnetSubnegotiation :: HasTelnetPtr t => t -> Option -> ByteString -> IO ()
telnetSubnegotiation t opt bs = withTelnetPtr t $
\telnetP -> F.telnetSubnegotiation telnetP opt bs
telnetBeginCompress2 :: HasTelnetPtr t => t -> IO ()
telnetBeginCompress2 t = withTelnetPtr t $
\telnetP -> F.cTelnetBeginCompress2 telnetP
telnetNewEnvironSend :: HasTelnetPtr t => t -> [(Var, ByteString)] -> IO ()
telnetNewEnvironSend t vars = withTelnetPtr t $
\telnetP -> do
let sendVar (var, str) = B8.useAsCString str (sendVal $ varToEvar var)
sendVal = F.cTelnetNewEnvironValue telnetP
F.cTelnetBeginNewEnviron telnetP T.eCmdSend
traverse_ sendVar vars
F.cTelnetIac telnetP I.iacSE
telnetNewEnviron
:: HasTelnetPtr t => t -> IsInfo -> [(Var, ByteString, ByteString)] -> IO ()
telnetNewEnviron t isInfo vars = withTelnetPtr t $
\telnetP -> do
let isInfo' = case isInfo of
Is -> T.eCmdIs
Info -> T.eCmdInfo
sendVar (var, name, value) =
B8.useAsCString name (sendVal $ varToEvar var) *>
B8.useAsCString value (sendVal T.eValue)
sendVal = F.cTelnetNewEnvironValue telnetP
F.cTelnetBeginNewEnviron telnetP isInfo'
traverse_ sendVar vars
F.cTelnetIac telnetP I.iacSE
telnetTTypeSend :: HasTelnetPtr t => t -> IO ()
telnetTTypeSend t = withTelnetPtr t F.cTelnetTTypeSend
telnetTTypeIs :: HasTelnetPtr t => t -> ByteString -> IO ()
telnetTTypeIs t bs = withTelnetPtr t $
\telnetP -> B8.useAsCString bs (F.cTelnetTTypeIs telnetP)
telnetSendZmp :: HasTelnetPtr t => t -> [ByteString] -> IO ()
telnetSendZmp t cmd = withTelnetPtr t $
\telnetP -> F.telnetSendZmp telnetP cmd
telnetSendMssp :: HasTelnetPtr t => t -> [(ByteString, [ByteString])] -> IO ()
telnetSendMssp t stats = telnetSubnegotiation t optMSSP stats' where
stats' = foldMap pack stats
pack (var, vals) = msspVar <> var <> foldMap (msspVal <>) vals
msspVar = B8.singleton . castCUCharToChar . T.unMsspVar $ T.msspVar
msspVal = B8.singleton . castCUCharToChar . T.unMsspVar $ T.msspVal
convertEventT :: T.EventT -> IO Event
convertEventT (T.Data (str, len)) =
Received <$> B8.packCStringLen (str, fromIntegral len)
convertEventT (T.Send (str, len)) =
Send <$> B8.packCStringLen (str, fromIntegral len)
convertEventT (T.Warning e) = Warning <$> packErrorT e
convertEventT (T.Error e) = Error <$> packErrorT e
convertEventT (T.Command cmd) = pure $ Iac cmd
convertEventT (T.Will opt) = pure $ Will opt
convertEventT (T.Wont opt) = pure $ Wont opt
convertEventT (T.Do opt) = pure $ Do opt
convertEventT (T.Dont opt) = pure $ Dont opt
convertEventT (T.Subnegotiation opt (str, len)) =
Subnegotiation opt <$> B8.packCStringLen (str, fromIntegral len)
convertEventT (T.Zmp (argv, argc)) =
Zmp <$> (traverse B8.packCString =<< peekArray (fromIntegral argc) argv)
convertEventT (T.TerminalType cmd name)
| cmd == T.tCmdIs = TerminalTypeIs <$> B8.packCString name
| cmd == T.tCmdSend = pure TerminalTypeSend
| otherwise = throwIO $ T.UnexpectedTerminalTypeCmd cmd
convertEventT (T.Compress ok) = pure . Compress $ ok == 1
convertEventT (T.Environ cmd (values, size)) = do
environs <- peekArray (fromIntegral size) values
if | cmd == T.eCmdSend -> do
vars <- traverse packVar environs
pure $ EnvironSend vars
| cmd `elem` [T.eCmdIs, T.eCmdInfo] -> do
isInfo <- if | cmd == T.eCmdIs -> pure Is
| cmd == T.eCmdInfo -> pure Info
| otherwise -> throwIO $ T.UnexpectedEnvironCmd cmd
vars <- traverse packVarVal environs
pure $ Environ isInfo vars
| otherwise -> throwIO $ T.UnexpectedEnvironCmd cmd
convertEventT (T.Mssp (values, size)) = do
environs <- peekArray (fromIntegral size) values
packed <- traverse packVarVal' environs
let grouped = groupBy ((==) `on` fst) packed
extract [] = error "Grouping should have made lists nonempty!"
extract ((var, val):vals) = (var, val : map snd vals)
pure . Mssp $ map extract grouped
varToEvar :: Var -> T.EVar
varToEvar Var = T.eVar
varToEvar UserVar = T.eUserVar
eVarToVar :: T.EVar -> Var
eVarToVar var
| var == T.eVar = Var
| var == T.eUserVar = UserVar
| otherwise = throw $ T.UnexpectedEnvironVar var
packErrorT :: T.ErrorT -> IO Err
packErrorT T.ErrorT{..} = do
file <- B8.packCString _file
func <- B8.packCString _func
msg <- B8.packCString _msg
pure $ Err file func msg (fromIntegral _line) _errcode
packVar :: T.TelnetEnvironT -> IO (Var, ByteString)
packVar T.TelnetEnvironT{..} = do
var <- B8.packCString _var
pure (eVarToVar _type, var)
packVarVal :: T.TelnetEnvironT -> IO (Var, ByteString, ByteString)
packVarVal T.TelnetEnvironT{..} = do
var <- B8.packCString _var
value <- B8.packCString _value
pure (eVarToVar _type, var, value)
packVarVal' :: T.TelnetEnvironT -> IO (ByteString, ByteString)
packVarVal' T.TelnetEnvironT{..} =
(,) <$> B8.packCString _var <*> B8.packCString _value
convertEventHandler :: EventHandler -> F.TelnetEventHandlerT
convertEventHandler f telnetP eventP _ =
peek eventP >>= convertEventT >>= f telnetP