hsrelp-0.2.1.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
RelpServerParams(..)
, runRelpServer
, runRelpTLSServer, runRelpTLSServer'
, 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 => RelpServerParams s = RelpServerParams
{ relpServerReport :: !(s -> SockAddr -> String -> String -> IO ())
, relpServerAccept :: !(s -> SockAddr -> RelpOffers -> IO Bool)
, relpServerNotify :: !(s -> SockAddr -> ByteString -> IO Bool)
, relpServerReduce :: !(s -> SockAddr -> IO ())
, relpServerFinish :: !(s -> SockAddr -> IO ())
}
-- | Relp server options stub
instance RelpFlow s => Default (RelpServerParams s) where
def = RelpServerParams
(\_ a t -> hPutStrLn stderr . ((show a <> " " <> t <> ": ")<>))
(\_ _ _ -> pure True)
(\_ _ _ -> pure True)
(\_ _ -> pure ())
(\_ _ -> pure ())
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
relpFlowRecv :: a -> IO B8.ByteString
relpFlowSend :: a -> B8.ByteString -> IO ()
relpFlowClose :: a -> IO ()
instance RelpFlow Socket where
relpFlowRecv = getContents
relpFlowSend = sendAll
relpFlowClose = S.close
instance RelpFlow Context where
relpFlowRecv = fmap B8.fromStrict . recvData
relpFlowSend = sendData
relpFlowClose = bye
-- | Provides a simple RELP server.
runRelpServer :: PortNumber -> RelpServerParams Socket -> IO ()
runRelpServer portnum opts = srv where
srv = runTCPServer Nothing portnum (buildRelpServerHandle opts)
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.
runRelpTLSServer' :: FilePath -> FilePath -> FilePath
-> (ServerParams -> ServerParams)
-> ServiceName -> RelpServerParams Context -> IO ()
runRelpTLSServer' fca fcr fky fhp snm cb = lcrd >>= \cr ->
readCertificateStore fca >>= \ca ->
runRelpTLSServer (fhp (makeServerParams cr ca)) HostAny snm cb where
lcrd = credentialLoadX509Chain fcr [fca] fky <&> either (error . show) id
-- | Provides a TLS RELP server
runRelpTLSServer :: ServerParams -> HostPreference
-> String -> RelpServerParams Context -> IO ()
runRelpTLSServer 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
=> RelpServerParams s -- ^ Server options
-> s -- ^ Some server socket
-> SockAddr -- ^ Client address
-> IO () -- ^ Never returns
buildRelpServerHandle RelpServerParams{..} = 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 <- relpFlowRecv s >>= processMessage s a
if status then handleMessage s a else handleClose s a
handleClose s a = relpServerReduce s a >> relpFlowClose 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 = relpFlowSend 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