mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-30 14:16:00 +00:00
168 lines
5.2 KiB
Haskell
168 lines
5.2 KiB
Haskell
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Main where
|
|
|
|
import Control.Monad (unless, when)
|
|
import qualified Crypto.Store.PKCS8 as S
|
|
import Data.ByteString.Base64 (encode)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import Data.Char (toLower)
|
|
import Data.Functor (($>))
|
|
import Data.Ini (lookupValue, readIniFile)
|
|
import qualified Data.Text as T
|
|
import Data.X509 (PrivKey (PrivKeyRSA))
|
|
import Options.Applicative
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.Server (runSMPServer)
|
|
import Simplex.Messaging.Server.Env.STM
|
|
import Simplex.Messaging.Server.StoreLog (StoreLog, openReadStoreLog)
|
|
import System.Directory (createDirectoryIfMissing, doesFileExist)
|
|
import System.Exit (exitFailure)
|
|
import System.FilePath (combine)
|
|
import System.IO (IOMode (..), hFlush, stdout)
|
|
|
|
cfg :: ServerConfig
|
|
cfg =
|
|
ServerConfig
|
|
{ tcpPort = "5223",
|
|
tbqSize = 16,
|
|
queueIdBytes = 12,
|
|
msgIdBytes = 6,
|
|
storeLog = Nothing,
|
|
-- key is loaded from the file server_key in /etc/opt/simplex directory
|
|
serverPrivateKey = undefined
|
|
}
|
|
|
|
newKeySize :: Int
|
|
newKeySize = 2048 `div` 8
|
|
|
|
cfgDir :: FilePath
|
|
cfgDir = "/etc/opt/simplex"
|
|
|
|
logDir :: FilePath
|
|
logDir = "/var/opt/simplex"
|
|
|
|
defaultStoreLogFile :: FilePath
|
|
defaultStoreLogFile = combine logDir "smp-server-store.log"
|
|
|
|
main :: IO ()
|
|
main = do
|
|
opts <- getServerOpts
|
|
putStrLn "SMP Server (-h for help)"
|
|
ini <- readCreateIni opts
|
|
storeLog <- openStoreLog ini
|
|
pk <- readCreateKey
|
|
B.putStrLn $ "transport key hash: " <> serverKeyHash pk
|
|
putStrLn $ "listening on port " <> tcpPort cfg
|
|
runSMPServer cfg {serverPrivateKey = pk, storeLog}
|
|
|
|
data IniOpts = IniOpts
|
|
{ enableStoreLog :: Bool,
|
|
storeLogFile :: FilePath
|
|
}
|
|
|
|
readCreateIni :: ServerOpts -> IO IniOpts
|
|
readCreateIni ServerOpts {configFile} = do
|
|
createDirectoryIfMissing True cfgDir
|
|
doesFileExist configFile >>= (`unless` createIni)
|
|
readIni
|
|
where
|
|
readIni :: IO IniOpts
|
|
readIni = do
|
|
ini <- either exitError pure =<< readIniFile configFile
|
|
let enableStoreLog = (== Right "on") $ lookupValue "STORE_LOG" "enable" ini
|
|
storeLogFile = either (const defaultStoreLogFile) T.unpack $ lookupValue "STORE_LOG" "file" ini
|
|
pure IniOpts {enableStoreLog, storeLogFile}
|
|
exitError e = do
|
|
putStrLn $ "error reading config file " <> configFile <> ": " <> e
|
|
exitFailure
|
|
createIni :: IO ()
|
|
createIni = do
|
|
confirm $ "Save default ini file to " <> configFile
|
|
writeFile
|
|
configFile
|
|
"[STORE_LOG]\n\
|
|
\# The server uses STM memory to store SMP queues and messages,\n\
|
|
\# that will be lost on restart (e.g., as with redis).\n\
|
|
\# This option enables saving SMP queues to append only log,\n\
|
|
\# and restoring them when the server is started.\n\
|
|
\# Log is compacted on start (deleted queues are removed).\n\
|
|
\# The messages in the queues are not logged.\n\
|
|
\\n\
|
|
\# enable: on\n\
|
|
\# file: /var/opt/simplex/smp-server-store.log\n"
|
|
|
|
readCreateKey :: IO C.FullPrivateKey
|
|
readCreateKey = do
|
|
createDirectoryIfMissing True cfgDir
|
|
let path = combine cfgDir "server_key"
|
|
hasKey <- doesFileExist path
|
|
(if hasKey then readKey else createKey) path
|
|
where
|
|
createKey :: FilePath -> IO C.FullPrivateKey
|
|
createKey path = do
|
|
confirm "Generate new server key pair"
|
|
(_, pk) <- C.generateKeyPair newKeySize
|
|
S.writeKeyFile S.TraditionalFormat path [PrivKeyRSA $ C.rsaPrivateKey pk]
|
|
pure pk
|
|
readKey :: FilePath -> IO C.FullPrivateKey
|
|
readKey path = do
|
|
S.readKeyFile path >>= \case
|
|
[S.Unprotected (PrivKeyRSA pk)] -> pure $ C.FullPrivateKey pk
|
|
[_] -> errorExit "not RSA key"
|
|
[] -> errorExit "invalid key file format"
|
|
_ -> errorExit "more than one key"
|
|
where
|
|
errorExit :: String -> IO b
|
|
errorExit e = putStrLn (e <> ": " <> path) >> exitFailure
|
|
|
|
confirm :: String -> IO ()
|
|
confirm msg = do
|
|
putStr $ msg <> " (y/N): "
|
|
hFlush stdout
|
|
ok <- getLine
|
|
when (map toLower ok /= "y") exitFailure
|
|
|
|
serverKeyHash :: C.FullPrivateKey -> B.ByteString
|
|
serverKeyHash = encode . C.unKeyHash . C.publicKeyHash . C.publicKey
|
|
|
|
openStoreLog :: IniOpts -> IO (Maybe (StoreLog 'ReadMode))
|
|
openStoreLog IniOpts {enableStoreLog, storeLogFile = f}
|
|
| enableStoreLog = do
|
|
createDirectoryIfMissing True logDir
|
|
putStrLn ("store log: " <> f)
|
|
Just <$> openReadStoreLog f
|
|
| otherwise = putStrLn "store log disabled" $> Nothing
|
|
|
|
newtype ServerOpts = ServerOpts
|
|
{ configFile :: FilePath
|
|
}
|
|
|
|
serverOpts :: Parser ServerOpts
|
|
serverOpts =
|
|
ServerOpts
|
|
<$> strOption
|
|
( long "config"
|
|
<> short 'c'
|
|
<> metavar "INI_FILE"
|
|
<> help ("config file (" <> defaultIniFile <> ")")
|
|
<> value defaultIniFile
|
|
)
|
|
where
|
|
defaultIniFile = combine cfgDir "smp-server.ini"
|
|
|
|
getServerOpts :: IO ServerOpts
|
|
getServerOpts = execParser opts
|
|
where
|
|
opts =
|
|
info
|
|
(serverOpts <**> helper)
|
|
( fullDesc
|
|
<> header "Simplex Messaging Protocol (SMP) Server"
|
|
<> progDesc "Start server with INI_FILE (created on first run)"
|
|
)
|