hsrelp-0.1.4.1: src/Network/RELP/Server.hs
-- | RELP (Reliable Event Logging Protocol) simple server
{-# LANGUAGE OverloadedStrings, RecordWildCards, ScopedTypeVariables #-}
module Network.RELP.Server
(
-- * Running a standalone RELP server
RelpServerOpts(..), 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.TLS
import Network.Simple.TCP.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.Default
import Data.ByteString (ByteString)
import Data.ByteString.UTF8 (toString)
import Data.Functor ( ($>), (<&>), void )
import Control.Applicative ( Alternative((<|>)) )
import Control.Monad (forever)
import Control.Concurrent
import System.IO hiding (getContents)
import Control.Exception (SomeException)
import qualified Control.Exception as E
-- | Relp server options
data RelpFlow s => RelpServerOpts s = RelpServerPrefs
{ relpServerReport :: s -> SockAddr -> String -> String -> IO ()
, relpServerAccept :: s -> SockAddr -> RelpOffers -> IO Bool
, relpServerNotify :: s -> RelpMessageHandler
, relpServerReduce :: s -> SockAddr -> IO ()
, relpServerFinish :: s -> SockAddr -> IO ()
}
-- | Relp server options stub
instance RelpFlow s => Default (RelpServerOpts s) where
def = RelpServerPrefs
(\_ a t -> hPutStrLn stderr . ((show a <> " " <> t <> ": ")<>))
(\_ _ _ -> pure True)
(\_ _ _ -> pure True)
(\_ _ -> pure ())
(\_ _ -> pure ())
-- | Message handler callback.
type RelpMessageHandler =
SockAddr -- ^ Client connection address
-> ByteString -- ^ Log message
-> IO 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 :: a -> IO B8.ByteString
flowSend :: a -> B8.ByteString -> IO ()
flowClose :: a -> IO ()
instance RelpFlow Socket where
flowRecv = getContents
flowSend = sendAll
flowClose = S.close
instance RelpFlow Context where
flowRecv = fmap B8.fromStrict . recvData
flowSend = sendData
flowClose = bye
-- | Provides a simple RELP server.
runRelpServer :: PortNumber -> RelpMessageHandler -> IO ()
runRelpServer portnum hdl = srv where
srv = runTCPServer Nothing portnum shl
shl = buildRelpServerHandle def{relpServerNotify = const hdl}
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 $
forkFinally (server conn peer) (const (S.gracefulClose conn 5000))
-- | Provides a simple TLS RELP server.
runTLSServer' :: FilePath -> FilePath -> FilePath
-> ServiceName -> RelpServerOpts Context -> IO ()
runTLSServer' fca fcr fky snm cb = lcrd >>= \cr ->
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 :: ServerParams -> HostPreference
-> String -> RelpServerOpts Context -> IO ()
runTLSServer params hostprefs servicename opts = srv where
srv = listen hostprefs servicename acp
acp (sock, _peer) = forever (acceptFork params sock opn)
opn (ctx, peer) = buildRelpServerHandle opts ctx peer
-- | Build relp messages handler.
buildRelpServerHandle :: RelpFlow s
=> RelpServerOpts s -- ^ Server options
-> s -- ^ Some server socket
-> SockAddr -- ^ Client address
-> IO () -- ^ Never returns
buildRelpServerHandle RelpServerPrefs{..} = safeRun where
safeRun s a = E.try (handleMessage s a) >>= check s a
check s a (Right ()) = relpServerFinish s a
check s a (Left (e :: SomeException)) =
relpServerReport s a "ERROR" (show e) >> relpServerFinish s a
handleMessage s a = do
status <- flowRecv s >>= processMessage s a
if status then handleMessage s a else handleClose s a
handleClose s a = relpServerReduce s a >> flowClose s
processMessage s a = parseLazy_ err process relpParser where
err e = relpServerReport s a "ERROR" ("parser: " ++ show e) $> 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
if versionValid then do
relpRsp s msg $ "200 OK "
++ "relp_version=0\nrelp_software=hsRELP\ncommands="
++ maybe "syslog" toString (lookup "commands" offers)
relpServerAccept s a offers
else do
relpServerReport s a "ERROR" ("unsupported version " <> show offers)
relpNAck s msg "unsupported RELP version" >> return False
process msg@RelpMessage{ relpCommand = RelpSYSLOG, relpData = txt } = do
status <- relpServerNotify s a txt
if status then relpAck s msg else relpNAck s msg "rejected"
return status
process msg = do
relpServerReport s a "ERROR" ("strange message command: " ++ show msg)
relpNAck s 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 :: RelpFlow s => s -> RelpMessage -> String -> IO ()
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 :: RelpFlow s => s -> RelpMessage -> IO ()
relpAck sock msg = relpRsp sock msg "200 OK"
relpNAck :: RelpFlow s => s -> RelpMessage -> String -> IO ()
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