hsrelp-0.1.3.1: src/Network/RELP/Server.hs
-- | RELP (Reliable Event Logging Protocol) simple server
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MonoLocalBinds #-}
module Network.RELP.Server
(
-- * Running a standalone RELP server
RelpMessageHandler
, runRelpServer
, runTLSServer, runTLSServer'
, RelpFlow
, buildRelpServerHandle
)
where
import Prelude hiding (getContents, take)
import Network.Socket (PortNumber, AddrInfo(..),
AddrInfoFlag(..), SocketType(..), SocketOption(..))
import qualified Network.Socket as S
import Network.Socket.ByteString.Lazy ( getContents, sendAll )
import qualified Data.ByteString as B
import Network.Simple.TCP.TLS
import Network.TLS
import Data.X509.CertificateStore
import Data.Attoparsec.ByteString (parseOnly, string, take,
takeWhile1, word8, many', Parser)
import qualified Data.Attoparsec.ByteString.Lazy as LBP
import qualified Data.ByteString.Lazy.Char8 as B8
import Data.ByteString (ByteString)
import Data.ByteString.UTF8 (toString)
import Data.Functor ( ($>), (<&>), void )
import Control.Applicative ( Alternative((<|>)) )
import Control.Monad (forever)
import Control.Monad.IO.Class
import Control.Monad.Catch
import System.IO hiding (getContents)
import qualified Control.Exception as E
-- | Message handler callback.
type RelpMessageHandler m = Monad m =>
SockAddr -- ^ Client connection address
-> ByteString -- ^ Log message
-> m Bool -- ^ Reject message (reply error RSP) if False
data RelpCommand = RelpRSP | RelpOPEN | RelpSYSLOG | RelpCLOSE
| RelpCommand ByteString
deriving (Show, Eq)
data RelpMessage = RelpMessage
{ relpTxnr :: Int
, relpCommand :: RelpCommand
, relpData :: ByteString
} deriving (Show, Eq)
type RelpOffers = [(ByteString, ByteString)]
class RelpFlow a where
flowRecv :: (MonadMask m, MonadIO m) => a -> m B8.ByteString
flowSend :: (MonadMask m, MonadIO m) => a -> B8.ByteString -> m ()
flowClose :: (MonadMask m, MonadIO m) => a -> m ()
instance RelpFlow Socket where
flowRecv = liftIO . getContents
flowSend s = liftIO . sendAll s
flowClose = liftIO . S.close
instance RelpFlow Context where
flowRecv = fmap B8.fromStrict . recvData
flowSend = sendData
flowClose = bye
-- | Provides a simple RELP server.
runRelpServer :: PortNumber -> RelpMessageHandler IO -> IO ()
runRelpServer portnum = runTCPServer Nothing portnum . buildRelpServerHandle where
runTCPServer mhost port server = withSocketsDo $ do
addr <- resolve
E.bracket (open addr) S.close loop
where
resolve = do
let hints = S.defaultHints {
addrFlags = [AI_PASSIVE]
, addrSocketType = Stream
}
head <$> S.getAddrInfo (Just hints) mhost (Just $ show port)
open addr = E.bracketOnError (S.openSocket addr) S.close $ \sock -> do
S.setSocketOption sock ReuseAddr 1
S.withFdSocket sock S.setCloseOnExecIfNeeded
S.bind sock $ addrAddress addr
S.listen sock 1024
return sock
loop sock = forever $ E.bracketOnError (S.accept sock) (S.close . fst)
$ \(conn, peer) -> void $
E.finally (server conn peer) (S.gracefulClose conn 5000)
-- | Provides a simple TLS RELP server.
runTLSServer' :: (MonadIO m, MonadMask m) => FilePath -> FilePath -> FilePath
-> ServiceName -> (SockAddr -> ByteString -> m Bool) -> m ()
runTLSServer' fca fcr fky snm cb = liftIO lcrd >>= \cr ->
liftIO (readCertificateStore fca) >>= \ca ->
runTLSServer (makeServerParams cr ca) HostAny snm cb where
lcrd = credentialLoadX509Chain fcr [fca] fky <&> either (error . show) id
-- | Provides a TLS RELP server
runTLSServer :: (MonadMask m, MonadIO m) => ServerParams -> HostPreference
-> String -> RelpMessageHandler m -> m ()
runTLSServer params hostprefs servicename server = srv where
srv = listen hostprefs servicename (\(sock, _peer) -> accept params sock open)
open (ctx, peer) = do
liftIO (hPutStrLn stderr (" …" <> show peer))
buildRelpServerHandle server ctx peer
-- | Build relp messages handler.
buildRelpServerHandle :: (MonadIO m, MonadMask m, RelpFlow sock)
=> RelpMessageHandler m -- ^ Message handler
-> sock -- ^ Server socket
-> SockAddr -- ^ Client address
-> m () -- ^ Never returns
buildRelpServerHandle cb = handleMessage where
handleMessage sock srcAddr = do
status <- flowRecv sock >>= processMessage sock srcAddr
if status then handleMessage sock srcAddr else flowClose sock
processMessage sock srcAddr = parseLazy_ err process relpParser
where
err e = liftIO (hPutStrLn stderr ("ERROR: parser: " ++ show e) >> return False)
process msg@RelpMessage{ relpCommand = RelpOPEN, relpData = txt } = do
let offers = parse_ (const []) id relpOffersParser txt
-- NOTE only version 0 supported!
let versionValid = (Just "0" ==) $ lookup "relp_version" offers
-- TODO FIXME check commands offer?
if versionValid then do
relpRsp sock msg $ "200 OK "
++ "relp_version=0\nrelp_software=hsRELP\ncommands="
++ maybe "syslog" toString (lookup "commands" offers)
return True
else relpNAck sock msg "unsupported RELP version" >> return False
process msg@RelpMessage{ relpCommand = RelpSYSLOG, relpData = txt } = do
status <- cb srcAddr txt
if status then relpAck sock msg else relpNAck sock msg "rejected"
return status
process msg = do
liftIO (hPutStrLn stderr ("ERROR: strange message command: " ++ show msg))
relpNAck sock msg "unexpected message command"
return False
relpParser :: Parser RelpMessage
relpParser = do
txnr <- decimal <* space
command <- parseCommand <* space
datalen <- decimal <* space
content <- take datalen
return $ RelpMessage txnr command content
where
decimal :: Integral a => Parser a
decimal = B.foldl' step 0 `fmap` takeWhile1 isDecimal where
step a c = a * 10 + fromIntegral (c - 48)
isDecimal c = c >= 48 && c <= 57
space = word8 32
parseCommand =
string "syslog" $> RelpSYSLOG
<|> string "close" $> RelpCLOSE
<|> string "open" $> RelpOPEN
<|> string "rsp" $> RelpRSP
<|> RelpCommand <$> takeWhile1 (/= 32)
relpOffersParser :: Parser RelpOffers
relpOffersParser = many' $ pair <* word8 sep
where
sep = 10 -- \n
der = 61 -- '='
pair = liftA2 (,)
(takeWhile1 (\c-> c /= der && c /= sep))
(word8 der *> takeWhile1 (/= sep) <|> return "")
relpRsp :: (MonadIO m, MonadMask m, RelpFlow s) => s -> RelpMessage -> String -> m ()
relpRsp sock msg reply = flowSend sock mkReply
-- putStrLn $ prettyHex $ B8.toStrict mkReply
where
mkReply = B8.pack $ show (relpTxnr msg) ++ " rsp "
++ show (length reply) ++ " " ++ reply ++ "\n"
relpAck :: (MonadIO m, MonadMask m, RelpFlow s) => s -> RelpMessage -> m ()
relpAck sock msg = relpRsp sock msg "200 OK"
relpNAck :: (MonadIO m, MonadMask m, RelpFlow s) => s -> RelpMessage -> String -> m ()
relpNAck sock msg err = relpRsp sock msg $ "500 " ++ err
-- just shortcuts
parse_ :: (String -> c) -> (b -> c) -> Parser b -> ByteString -> c
parse_ err ok p = either err ok . parseOnly p
parseLazy_ :: (String -> c) -> (b -> c) -> Parser b -> B8.ByteString -> c
parseLazy_ err ok p = either err ok . LBP.eitherResult . LBP.parse p