mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-05-24 16:55:24 +00:00
e1002d5ac0
* GADTs for cryptographic keys * update tests (signature timing tests still fail) * fix signature verification timing tests * configurable algorithm to sign commands to SMP queues (Ed448 by default) * add dummy Ed keys, add timing tests for Ed keys * re-enable Connection subscriptions tests
160 lines
6.3 KiB
Haskell
160 lines
6.3 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE GADTs #-}
|
|
{-# LANGUAGE KindSignatures #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
|
|
module Simplex.Messaging.Server.StoreLog
|
|
( StoreLog, -- constructors are not exported
|
|
openWriteStoreLog,
|
|
openReadStoreLog,
|
|
storeLogFilePath,
|
|
closeStoreLog,
|
|
logCreateQueue,
|
|
logSecureQueue,
|
|
logAddNotifier,
|
|
logDeleteQueue,
|
|
readWriteStoreLog,
|
|
)
|
|
where
|
|
|
|
import Control.Applicative (optional, (<|>))
|
|
import Control.Monad (unless)
|
|
import Data.Attoparsec.ByteString.Char8 (Parser)
|
|
import qualified Data.Attoparsec.ByteString.Char8 as A
|
|
import Data.Bifunctor (first, second)
|
|
import Data.ByteString.Base64 (encode)
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import qualified Data.ByteString.Lazy.Char8 as LB
|
|
import Data.Either (partitionEithers)
|
|
import Data.Functor (($>))
|
|
import Data.List (foldl')
|
|
import Data.Map.Strict (Map)
|
|
import qualified Data.Map.Strict as M
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Parsers (base64P, parseAll)
|
|
import Simplex.Messaging.Protocol
|
|
import Simplex.Messaging.Server.QueueStore (QueueRec (..), QueueStatus (..))
|
|
import Simplex.Messaging.Transport (trimCR)
|
|
import System.Directory (doesFileExist)
|
|
import System.IO
|
|
|
|
-- | opaque container for file handle with a type-safe IOMode
|
|
-- constructors are not exported, openWriteStoreLog and openReadStoreLog should be used instead
|
|
data StoreLog (a :: IOMode) where
|
|
ReadStoreLog :: FilePath -> Handle -> StoreLog 'ReadMode
|
|
WriteStoreLog :: FilePath -> Handle -> StoreLog 'WriteMode
|
|
|
|
data StoreLogRecord
|
|
= CreateQueue QueueRec
|
|
| SecureQueue QueueId SenderPublicKey
|
|
| AddNotifier QueueId NotifierId NotifierPublicKey
|
|
| DeleteQueue QueueId
|
|
|
|
storeLogRecordP :: Parser StoreLogRecord
|
|
storeLogRecordP =
|
|
"CREATE " *> createQueueP
|
|
<|> "SECURE " *> secureQueueP
|
|
<|> "NOTIFIER " *> addNotifierP
|
|
<|> "DELETE " *> (DeleteQueue <$> base64P)
|
|
where
|
|
createQueueP = CreateQueue <$> queueRecP
|
|
secureQueueP = SecureQueue <$> base64P <* A.space <*> C.strKeyP
|
|
addNotifierP =
|
|
AddNotifier <$> base64P <* A.space <*> base64P <* A.space <*> C.strKeyP
|
|
queueRecP = do
|
|
recipientId <- "rid=" *> base64P <* A.space
|
|
senderId <- "sid=" *> base64P <* A.space
|
|
recipientKey <- "rk=" *> C.strKeyP <* A.space
|
|
senderKey <- "sk=" *> optional C.strKeyP
|
|
notifier <- optional $ (,) <$> (" nid=" *> base64P) <*> (" nk=" *> C.strKeyP)
|
|
pure QueueRec {recipientId, senderId, recipientKey, senderKey, notifier, status = QueueActive}
|
|
|
|
serializeStoreLogRecord :: StoreLogRecord -> ByteString
|
|
serializeStoreLogRecord = \case
|
|
CreateQueue q -> "CREATE " <> serializeQueue q
|
|
SecureQueue rId sKey -> "SECURE " <> encode rId <> " " <> C.serializeKey sKey
|
|
AddNotifier rId nId nKey -> B.unwords ["NOTIFIER", encode rId, encode nId, C.serializeKey nKey]
|
|
DeleteQueue rId -> "DELETE " <> encode rId
|
|
where
|
|
serializeQueue QueueRec {recipientId, senderId, recipientKey, senderKey, notifier} =
|
|
B.unwords
|
|
[ "rid=" <> encode recipientId,
|
|
"sid=" <> encode senderId,
|
|
"rk=" <> C.serializeKey recipientKey,
|
|
"sk=" <> maybe "" C.serializeKey senderKey
|
|
]
|
|
<> maybe "" serializeNotifier notifier
|
|
serializeNotifier (nId, nKey) = " nid=" <> encode nId <> " nk=" <> C.serializeKey nKey
|
|
|
|
openWriteStoreLog :: FilePath -> IO (StoreLog 'WriteMode)
|
|
openWriteStoreLog f = WriteStoreLog f <$> openFile f WriteMode
|
|
|
|
openReadStoreLog :: FilePath -> IO (StoreLog 'ReadMode)
|
|
openReadStoreLog f = do
|
|
doesFileExist f >>= (`unless` writeFile f "")
|
|
ReadStoreLog f <$> openFile f ReadMode
|
|
|
|
storeLogFilePath :: StoreLog a -> FilePath
|
|
storeLogFilePath = \case
|
|
WriteStoreLog f _ -> f
|
|
ReadStoreLog f _ -> f
|
|
|
|
closeStoreLog :: StoreLog a -> IO ()
|
|
closeStoreLog = \case
|
|
WriteStoreLog _ h -> hClose h
|
|
ReadStoreLog _ h -> hClose h
|
|
|
|
writeStoreLogRecord :: StoreLog 'WriteMode -> StoreLogRecord -> IO ()
|
|
writeStoreLogRecord (WriteStoreLog _ h) r = do
|
|
B.hPutStrLn h $ serializeStoreLogRecord r
|
|
hFlush h
|
|
|
|
logCreateQueue :: StoreLog 'WriteMode -> QueueRec -> IO ()
|
|
logCreateQueue s = writeStoreLogRecord s . CreateQueue
|
|
|
|
logSecureQueue :: StoreLog 'WriteMode -> QueueId -> SenderPublicKey -> IO ()
|
|
logSecureQueue s qId sKey = writeStoreLogRecord s $ SecureQueue qId sKey
|
|
|
|
logAddNotifier :: StoreLog 'WriteMode -> QueueId -> NotifierId -> NotifierPublicKey -> IO ()
|
|
logAddNotifier s qId nId nKey = writeStoreLogRecord s $ AddNotifier qId nId nKey
|
|
|
|
logDeleteQueue :: StoreLog 'WriteMode -> QueueId -> IO ()
|
|
logDeleteQueue s = writeStoreLogRecord s . DeleteQueue
|
|
|
|
readWriteStoreLog :: StoreLog 'ReadMode -> IO (Map RecipientId QueueRec, StoreLog 'WriteMode)
|
|
readWriteStoreLog s@(ReadStoreLog f _) = do
|
|
qs <- readQueues s
|
|
closeStoreLog s
|
|
s' <- openWriteStoreLog f
|
|
writeQueues s' qs
|
|
pure (qs, s')
|
|
|
|
writeQueues :: StoreLog 'WriteMode -> Map RecipientId QueueRec -> IO ()
|
|
writeQueues s = mapM_ (writeStoreLogRecord s . CreateQueue) . M.filter active
|
|
where
|
|
active QueueRec {status} = status == QueueActive
|
|
|
|
type LogParsingError = (String, ByteString)
|
|
|
|
readQueues :: StoreLog 'ReadMode -> IO (Map RecipientId QueueRec)
|
|
readQueues (ReadStoreLog _ h) = LB.hGetContents h >>= returnResult . procStoreLog
|
|
where
|
|
procStoreLog :: LB.ByteString -> ([LogParsingError], Map RecipientId QueueRec)
|
|
procStoreLog = second (foldl' procLogRecord M.empty) . partitionEithers . map parseLogRecord . LB.lines
|
|
returnResult :: ([LogParsingError], Map RecipientId QueueRec) -> IO (Map RecipientId QueueRec)
|
|
returnResult (errs, res) = mapM_ printError errs $> res
|
|
parseLogRecord :: LB.ByteString -> Either LogParsingError StoreLogRecord
|
|
parseLogRecord = (\s -> first (,s) $ parseAll storeLogRecordP s) . trimCR . LB.toStrict
|
|
procLogRecord :: Map RecipientId QueueRec -> StoreLogRecord -> Map RecipientId QueueRec
|
|
procLogRecord m = \case
|
|
CreateQueue q -> M.insert (recipientId q) q m
|
|
SecureQueue qId sKey -> M.adjust (\q -> q {senderKey = Just sKey}) qId m
|
|
AddNotifier qId nId nKey -> M.adjust (\q -> q {notifier = Just (nId, nKey)}) qId m
|
|
DeleteQueue qId -> M.delete qId m
|
|
printError :: LogParsingError -> IO ()
|
|
printError (e, s) = B.putStrLn $ "Error parsing log: " <> B.pack e <> " - " <> s
|