|
|
|
@@ -8,12 +8,11 @@
|
|
|
|
|
module Main where
|
|
|
|
|
|
|
|
|
|
import Control.Monad.Except
|
|
|
|
|
import Control.Monad.Trans.Except
|
|
|
|
|
import qualified Data.ByteString.Char8 as B
|
|
|
|
|
import Data.Char (toLower)
|
|
|
|
|
import Data.Composition ((.:))
|
|
|
|
|
import Data.Either (fromRight)
|
|
|
|
|
import Data.Ini (Ini, lookupValue, readIniFile)
|
|
|
|
|
import Data.List (dropWhileEnd)
|
|
|
|
|
import Data.Text (Text)
|
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import Network.Socket (ServiceName)
|
|
|
|
|
import Options.Applicative
|
|
|
|
@@ -22,147 +21,254 @@ import Simplex.Messaging.Server.Env.STM
|
|
|
|
|
import Simplex.Messaging.Server.StoreLog (StoreLog, openReadStoreLog, storeLogFilePath)
|
|
|
|
|
import Simplex.Messaging.Transport (ATransport (..), TLS, Transport (..), encodeFingerprint, loadFingerprint, simplexMQVersion)
|
|
|
|
|
import Simplex.Messaging.Transport.WebSockets (WS)
|
|
|
|
|
import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, removeDirectoryRecursive, removeFile)
|
|
|
|
|
import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, removeDirectoryRecursive)
|
|
|
|
|
import System.Exit (exitFailure)
|
|
|
|
|
import System.FilePath (combine)
|
|
|
|
|
import System.IO (IOMode (..), hFlush, stdout)
|
|
|
|
|
import System.IO (IOMode (..))
|
|
|
|
|
import System.Process (readCreateProcess, shell)
|
|
|
|
|
|
|
|
|
|
defaultServerPort :: ServiceName
|
|
|
|
|
defaultServerPort = "5223"
|
|
|
|
|
|
|
|
|
|
serverConfig :: ServerConfig
|
|
|
|
|
serverConfig =
|
|
|
|
|
ServerConfig
|
|
|
|
|
{ tbqSize = 16,
|
|
|
|
|
serverTbqSize = 128,
|
|
|
|
|
msgQueueQuota = 256,
|
|
|
|
|
queueIdBytes = 24,
|
|
|
|
|
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
|
|
|
|
|
-- below parameters are set based on ini file /etc/opt/simplex/smp-server.ini
|
|
|
|
|
transports = undefined,
|
|
|
|
|
storeLog = undefined,
|
|
|
|
|
caCertificateFile = undefined,
|
|
|
|
|
serverPrivateKeyFile = undefined,
|
|
|
|
|
serverCertificateFile = undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newKeySize :: Int
|
|
|
|
|
newKeySize = 2048 `div` 8
|
|
|
|
|
|
|
|
|
|
cfgDir :: FilePath
|
|
|
|
|
cfgDir = "/etc/opt/simplex"
|
|
|
|
|
|
|
|
|
|
logDir :: FilePath
|
|
|
|
|
logDir = "/var/opt/simplex"
|
|
|
|
|
|
|
|
|
|
-- TODO remove file paths from ini
|
|
|
|
|
defaultStoreLogFile :: FilePath
|
|
|
|
|
defaultStoreLogFile = combine logDir "smp-server-store.log"
|
|
|
|
|
|
|
|
|
|
iniFile :: FilePath
|
|
|
|
|
iniFile = combine cfgDir "smp-server.ini"
|
|
|
|
|
|
|
|
|
|
caPrivateKeyFile :: FilePath
|
|
|
|
|
caPrivateKeyFile = combine cfgDir "ca.key"
|
|
|
|
|
storeLogFile :: FilePath
|
|
|
|
|
storeLogFile = combine logDir "smp-server-store.log"
|
|
|
|
|
|
|
|
|
|
defaultCACertificateFile :: FilePath
|
|
|
|
|
defaultCACertificateFile = combine cfgDir "ca.crt"
|
|
|
|
|
caKeyFile :: FilePath
|
|
|
|
|
caKeyFile = combine cfgDir "ca.key"
|
|
|
|
|
|
|
|
|
|
defaultPrivateKeyFile :: FilePath
|
|
|
|
|
defaultPrivateKeyFile = combine cfgDir "server.key"
|
|
|
|
|
caCrtFile :: FilePath
|
|
|
|
|
caCrtFile = combine cfgDir "ca.crt"
|
|
|
|
|
|
|
|
|
|
defaultCertificateFile :: FilePath
|
|
|
|
|
defaultCertificateFile = combine cfgDir "server.crt"
|
|
|
|
|
serverKeyFile :: FilePath
|
|
|
|
|
serverKeyFile = combine cfgDir "server.key"
|
|
|
|
|
|
|
|
|
|
serverCrtFile :: FilePath
|
|
|
|
|
serverCrtFile = combine cfgDir "server.crt"
|
|
|
|
|
|
|
|
|
|
fingerprintFile :: FilePath
|
|
|
|
|
fingerprintFile = combine cfgDir "fingerprint"
|
|
|
|
|
|
|
|
|
|
main :: IO ()
|
|
|
|
|
main = do
|
|
|
|
|
opts <- getServerOpts
|
|
|
|
|
checkPubkeyAlgorihtm $ pubkeyAlgorihtm opts
|
|
|
|
|
case serverCommand opts of
|
|
|
|
|
ServerInit ->
|
|
|
|
|
runExceptT (getConfig opts) >>= \case
|
|
|
|
|
Right cfg -> do
|
|
|
|
|
putStrLn "Error: server is already initialized. Start it with `smp-server start` command"
|
|
|
|
|
fingerprint <- loadSavedFingerprint
|
|
|
|
|
printConfig cfg fingerprint
|
|
|
|
|
checkCAPrivateKeyFile
|
|
|
|
|
exitFailure
|
|
|
|
|
Left _ -> do
|
|
|
|
|
cfg <- initializeServer opts
|
|
|
|
|
putStrLn "Server was initialized. Start it with `smp-server start` command"
|
|
|
|
|
fingerprint <- loadSavedFingerprint
|
|
|
|
|
printConfig cfg fingerprint
|
|
|
|
|
warnCAPrivateKeyFile
|
|
|
|
|
ServerStart ->
|
|
|
|
|
runExceptT (getConfig opts) >>= \case
|
|
|
|
|
Right cfg -> runServer cfg
|
|
|
|
|
Left e -> do
|
|
|
|
|
putStrLn $ "Server is not initialized: " <> e
|
|
|
|
|
putStrLn "Initialize server with `smp-server init` command"
|
|
|
|
|
exitFailure
|
|
|
|
|
ServerDelete -> do
|
|
|
|
|
deleteServer
|
|
|
|
|
putStrLn "Server configuration and log files deleted"
|
|
|
|
|
getCliOptions >>= \opts -> case optCommand opts of
|
|
|
|
|
Init initOptions@InitOptions {pubkeyAlgorithm} -> do
|
|
|
|
|
-- TODO check during parsing
|
|
|
|
|
checkPubkeyAlgorithm pubkeyAlgorithm
|
|
|
|
|
doesFileExist iniFile >>= \case
|
|
|
|
|
True -> iniAlreadyExistsErr >> exitFailure
|
|
|
|
|
False -> initializeServer initOptions
|
|
|
|
|
Start -> do
|
|
|
|
|
doesFileExist iniFile >>= \case
|
|
|
|
|
False -> iniDoesNotExistErr >> exitFailure
|
|
|
|
|
True -> readIniFile iniFile >>= either (\e -> putStrLn e >> exitFailure) (runServer . mkIniOptions)
|
|
|
|
|
Delete -> cleanup >> putStrLn "Deleted configuration and log files"
|
|
|
|
|
where
|
|
|
|
|
checkPubkeyAlgorihtm :: String -> IO ()
|
|
|
|
|
checkPubkeyAlgorihtm alg
|
|
|
|
|
checkPubkeyAlgorithm alg
|
|
|
|
|
| alg == "ED448" || alg == "ED25519" = pure ()
|
|
|
|
|
| otherwise = putStrLn ("unsupported public-key algorithm " <> alg) >> exitFailure
|
|
|
|
|
| otherwise = putStrLn ("Unsupported public key algorithm " <> alg) >> exitFailure
|
|
|
|
|
iniAlreadyExistsErr = putStrLn $ "Error: server is already initialized (" <> iniFile <> " exists).\nRun `smp-server start`."
|
|
|
|
|
iniDoesNotExistErr = putStrLn $ "Error: server is not initialized (" <> iniFile <> " does not exist).\nRun `smp-server init`."
|
|
|
|
|
|
|
|
|
|
getConfig :: ServerOpts -> ExceptT String IO ServerConfig
|
|
|
|
|
getConfig opts = do
|
|
|
|
|
ini <- readIni
|
|
|
|
|
storeLog <- liftIO $ openStoreLog opts ini
|
|
|
|
|
pure $ makeConfig ini storeLog
|
|
|
|
|
newtype CliOptions = CliOptions {optCommand :: Command}
|
|
|
|
|
|
|
|
|
|
makeConfig :: IniOpts -> Maybe (StoreLog 'ReadMode) -> ServerConfig
|
|
|
|
|
makeConfig IniOpts {serverPort, enableWebsockets, caCertificateFile, serverPrivateKeyFile, serverCertificateFile} storeLog =
|
|
|
|
|
let transports = (serverPort, transport @TLS) : [("80", transport @WS) | enableWebsockets]
|
|
|
|
|
in serverConfig {transports, storeLog, caCertificateFile, serverPrivateKeyFile, serverCertificateFile}
|
|
|
|
|
data Command
|
|
|
|
|
= Init InitOptions
|
|
|
|
|
| Start
|
|
|
|
|
| Delete
|
|
|
|
|
|
|
|
|
|
printConfig :: ServerConfig -> String -> IO ()
|
|
|
|
|
printConfig ServerConfig {storeLog} fingerprint = do
|
|
|
|
|
putStrLn $ "SMP server v" <> simplexMQVersion
|
|
|
|
|
putStrLn $ "fingerprint: " <> fingerprint
|
|
|
|
|
putStrLn $ case storeLog of
|
|
|
|
|
Just s -> "store log: " <> storeLogFilePath s
|
|
|
|
|
Nothing -> "store log disabled"
|
|
|
|
|
data InitOptions = InitOptions
|
|
|
|
|
{ enableStoreLog :: Bool,
|
|
|
|
|
pubkeyAlgorithm :: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initializeServer :: ServerOpts -> IO ServerConfig
|
|
|
|
|
initializeServer opts = do
|
|
|
|
|
getCliOptions :: IO CliOptions
|
|
|
|
|
getCliOptions =
|
|
|
|
|
customExecParser
|
|
|
|
|
(prefs showHelpOnEmpty)
|
|
|
|
|
( info
|
|
|
|
|
(helper <*> versionOption <*> cliOptionsP)
|
|
|
|
|
(header version <> fullDesc)
|
|
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
versionOption = infoOption version (long "version" <> short 'v' <> help "Show version")
|
|
|
|
|
|
|
|
|
|
cliOptionsP :: Parser CliOptions
|
|
|
|
|
cliOptionsP =
|
|
|
|
|
CliOptions
|
|
|
|
|
<$> hsubparser
|
|
|
|
|
( command "init" (info initP (progDesc $ "Initialize server - creates " <> cfgDir <> " and " <> logDir <> " directories and configuration files"))
|
|
|
|
|
<> command "start" (info (pure Start) (progDesc $ "Start server (configuration: " <> iniFile <> ")"))
|
|
|
|
|
<> command "delete" (info (pure Delete) (progDesc "Delete configuration and log files"))
|
|
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
initP :: Parser Command
|
|
|
|
|
initP =
|
|
|
|
|
Init .: InitOptions
|
|
|
|
|
<$> switch
|
|
|
|
|
( long "store-log"
|
|
|
|
|
<> short 'l'
|
|
|
|
|
<> help "Enable store log for SMP queues persistence"
|
|
|
|
|
)
|
|
|
|
|
<*> strOption
|
|
|
|
|
( long "pubkey-algorithm"
|
|
|
|
|
<> short 'a'
|
|
|
|
|
<> help "Public key algorithm used for certificate generation: ED25519, ED448"
|
|
|
|
|
<> value "ED448"
|
|
|
|
|
<> showDefault
|
|
|
|
|
<> metavar "ALG"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
initializeServer :: InitOptions -> IO ()
|
|
|
|
|
initializeServer InitOptions {enableStoreLog, pubkeyAlgorithm} = do
|
|
|
|
|
cleanup
|
|
|
|
|
createDirectoryIfMissing True cfgDir
|
|
|
|
|
ini <- createIni opts
|
|
|
|
|
createX509 ini opts
|
|
|
|
|
saveFingerprint $ caCertificateFile (ini :: IniOpts)
|
|
|
|
|
storeLog <- openStoreLog opts ini
|
|
|
|
|
pure $ makeConfig ini storeLog
|
|
|
|
|
createDirectoryIfMissing True logDir
|
|
|
|
|
createX509
|
|
|
|
|
saveFingerprint
|
|
|
|
|
createIni
|
|
|
|
|
putStrLn $ "Server initialized, you can modify configuration in " <> iniFile <> ".\nRun `smp-server start` to start server."
|
|
|
|
|
printServiceInfo
|
|
|
|
|
warnCAPrivateKeyFile
|
|
|
|
|
where
|
|
|
|
|
createX509 = do
|
|
|
|
|
createOpensslConf
|
|
|
|
|
-- CA certificate (identity/offline)
|
|
|
|
|
run $ "openssl genpkey -algorithm " <> pubkeyAlgorithm <> " -out " <> caKeyFile
|
|
|
|
|
run $ "openssl req -new -x509 -days 999999 -config " <> opensslCnfFile <> " -extensions v3_ca -key " <> caKeyFile <> " -out " <> caCrtFile
|
|
|
|
|
-- server certificate (online)
|
|
|
|
|
run $ "openssl genpkey -algorithm " <> pubkeyAlgorithm <> " -out " <> serverKeyFile
|
|
|
|
|
run $ "openssl req -new -config " <> opensslCnfFile <> " -reqexts v3_req -key " <> serverKeyFile <> " -out " <> serverCsrFile
|
|
|
|
|
run $ "openssl x509 -req -days 999999 -copy_extensions copy -in " <> serverCsrFile <> " -CA " <> caCrtFile <> " -CAkey " <> caKeyFile <> " -out " <> serverCrtFile
|
|
|
|
|
where
|
|
|
|
|
run cmd = void $ readCreateProcess (shell cmd) ""
|
|
|
|
|
opensslCnfFile = combine cfgDir "openssl.cnf"
|
|
|
|
|
serverCsrFile = combine cfgDir "server.csr"
|
|
|
|
|
createOpensslConf =
|
|
|
|
|
-- TODO revise https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3, https://www.rfc-editor.org/rfc/rfc3279#section-2.3.5
|
|
|
|
|
writeFile
|
|
|
|
|
opensslCnfFile
|
|
|
|
|
"[req]\n\
|
|
|
|
|
\distinguished_name = req_distinguished_name\n\
|
|
|
|
|
\prompt = no\n\n\
|
|
|
|
|
\[req_distinguished_name]\n\
|
|
|
|
|
\CN = localhost\n\n\
|
|
|
|
|
\[v3_ca]\n\
|
|
|
|
|
\subjectKeyIdentifier = hash\n\
|
|
|
|
|
\authorityKeyIdentifier = keyid:always\n\
|
|
|
|
|
\basicConstraints = critical,CA:true\n\n\
|
|
|
|
|
\[v3_req]\n\
|
|
|
|
|
\basicConstraints = CA:FALSE\n\
|
|
|
|
|
\keyUsage = digitalSignature, nonRepudiation, keyAgreement\n\
|
|
|
|
|
\extendedKeyUsage = serverAuth\n"
|
|
|
|
|
|
|
|
|
|
runServer :: ServerConfig -> IO ()
|
|
|
|
|
runServer cfg = do
|
|
|
|
|
savedFingerprint <- loadSavedFingerprint
|
|
|
|
|
checkSavedFingerprint savedFingerprint
|
|
|
|
|
printConfig cfg savedFingerprint
|
|
|
|
|
saveFingerprint = do
|
|
|
|
|
fingerprint <- loadFingerprint caCrtFile
|
|
|
|
|
writeFile fingerprintFile $ (B.unpack . encodeFingerprint) fingerprint <> "\n"
|
|
|
|
|
|
|
|
|
|
createIni = do
|
|
|
|
|
writeFile iniFile $
|
|
|
|
|
"[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"
|
|
|
|
|
<> ("enable: " <> (if enableStoreLog then "on" else "off # on") <> "\n\n")
|
|
|
|
|
<> "[TRANSPORT]\n\
|
|
|
|
|
\port: 5223\n\
|
|
|
|
|
\websockets: on\n"
|
|
|
|
|
|
|
|
|
|
data IniOptions = IniOptions
|
|
|
|
|
{ enableStoreLog :: Bool,
|
|
|
|
|
port :: ServiceName,
|
|
|
|
|
enableWebsockets :: Bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
-- TODO ? properly parse ini as a whole
|
|
|
|
|
mkIniOptions :: Ini -> IniOptions
|
|
|
|
|
mkIniOptions ini =
|
|
|
|
|
IniOptions
|
|
|
|
|
{ enableStoreLog = (== "on") $ strict "STORE_LOG" "enable",
|
|
|
|
|
port = T.unpack $ strict "TRANSPORT" "port",
|
|
|
|
|
enableWebsockets = (== "on") $ strict "TRANSPORT" "websockets"
|
|
|
|
|
}
|
|
|
|
|
where
|
|
|
|
|
strict :: String -> String -> T.Text
|
|
|
|
|
strict section key =
|
|
|
|
|
fromRight (error ("no key " <> key <> " in section " <> section)) $
|
|
|
|
|
lookupValue (T.pack section) (T.pack key) ini
|
|
|
|
|
|
|
|
|
|
runServer :: IniOptions -> IO ()
|
|
|
|
|
runServer IniOptions {enableStoreLog, port, enableWebsockets} = do
|
|
|
|
|
checkSavedFingerprint
|
|
|
|
|
printServiceInfo
|
|
|
|
|
checkCAPrivateKeyFile
|
|
|
|
|
forM_ (transports cfg) $ \(port, ATransport t) ->
|
|
|
|
|
putStrLn $ "listening on port " <> port <> " (" <> transportName t <> ")"
|
|
|
|
|
cfg <- setupServerConfig
|
|
|
|
|
printServerConfig cfg
|
|
|
|
|
runSMPServer cfg
|
|
|
|
|
where
|
|
|
|
|
checkSavedFingerprint :: String -> IO ()
|
|
|
|
|
checkSavedFingerprint savedFingerprint = do
|
|
|
|
|
fingerprint <- loadFingerprint $ caCertificateFile (cfg :: ServerConfig)
|
|
|
|
|
if savedFingerprint == (B.unpack . encodeFingerprint) fingerprint
|
|
|
|
|
then putStrLn "stored fingerprint is valid"
|
|
|
|
|
else putStrLn "stored fingerprint is invalid" >> exitFailure
|
|
|
|
|
checkSavedFingerprint = do
|
|
|
|
|
savedFingerprint <- loadSavedFingerprint
|
|
|
|
|
fingerprint <- loadFingerprint caCrtFile
|
|
|
|
|
when (savedFingerprint /= (B.unpack . encodeFingerprint) fingerprint) $
|
|
|
|
|
putStrLn "Stored fingerprint is invalid." >> exitFailure
|
|
|
|
|
|
|
|
|
|
checkCAPrivateKeyFile :: IO ()
|
|
|
|
|
checkCAPrivateKeyFile =
|
|
|
|
|
doesFileExist caPrivateKeyFile >>= (`when` (alert >> warnCAPrivateKeyFile))
|
|
|
|
|
checkCAPrivateKeyFile =
|
|
|
|
|
doesFileExist caKeyFile >>= (`when` (alert >> warnCAPrivateKeyFile))
|
|
|
|
|
where
|
|
|
|
|
alert = putStrLn $ "WARNING: " <> caKeyFile <> " is present on the server!"
|
|
|
|
|
|
|
|
|
|
setupServerConfig = do
|
|
|
|
|
storeLog <- openStoreLog
|
|
|
|
|
let transports = (port, transport @TLS) : [("80", transport @WS) | enableWebsockets]
|
|
|
|
|
pure
|
|
|
|
|
ServerConfig
|
|
|
|
|
{ tbqSize = 16,
|
|
|
|
|
serverTbqSize = 128,
|
|
|
|
|
msgQueueQuota = 256,
|
|
|
|
|
queueIdBytes = 24,
|
|
|
|
|
msgIdBytes = 24, -- must be at least 24 bytes, it is used as 192-bit nonce for XSalsa20
|
|
|
|
|
caCertificateFile = caCrtFile,
|
|
|
|
|
privateKeyFile = serverKeyFile,
|
|
|
|
|
certificateFile = serverCrtFile,
|
|
|
|
|
transports,
|
|
|
|
|
storeLog
|
|
|
|
|
}
|
|
|
|
|
where
|
|
|
|
|
openStoreLog :: IO (Maybe (StoreLog 'ReadMode))
|
|
|
|
|
openStoreLog
|
|
|
|
|
| enableStoreLog = Just <$> openReadStoreLog storeLogFile
|
|
|
|
|
| otherwise = pure Nothing
|
|
|
|
|
|
|
|
|
|
printServerConfig ServerConfig {storeLog, transports} = do
|
|
|
|
|
putStrLn $ case storeLog of
|
|
|
|
|
Just s -> "Store log: " <> storeLogFilePath s
|
|
|
|
|
Nothing -> "Store log disabled."
|
|
|
|
|
forM_ transports $ \(p, ATransport t) ->
|
|
|
|
|
putStrLn $ "Listening on port " <> p <> " (" <> transportName t <> ")..."
|
|
|
|
|
|
|
|
|
|
cleanup :: IO ()
|
|
|
|
|
cleanup = do
|
|
|
|
|
deleteDirIfExists cfgDir
|
|
|
|
|
deleteDirIfExists logDir
|
|
|
|
|
where
|
|
|
|
|
alert = putStrLn $ "WARNING: " <> caPrivateKeyFile <> " is present on the server!"
|
|
|
|
|
deleteDirIfExists path = doesDirectoryExist path >>= (`when` removeDirectoryRecursive path)
|
|
|
|
|
|
|
|
|
|
printServiceInfo :: IO ()
|
|
|
|
|
printServiceInfo = do
|
|
|
|
|
putStrLn version
|
|
|
|
|
fingerprint <- loadSavedFingerprint
|
|
|
|
|
putStrLn $ "Fingerprint: " <> fingerprint
|
|
|
|
|
|
|
|
|
|
version :: String
|
|
|
|
|
version = "SMP server v" <> simplexMQVersion
|
|
|
|
|
|
|
|
|
|
warnCAPrivateKeyFile :: IO ()
|
|
|
|
|
warnCAPrivateKeyFile =
|
|
|
|
@@ -171,200 +277,10 @@ warnCAPrivateKeyFile =
|
|
|
|
|
\We highly recommend to remove CA private key file from the server and keep it securely in place of your choosing.\n\
|
|
|
|
|
\In case server's TLS credential is compromised you will be able to regenerate it using this key,\n\
|
|
|
|
|
\thus keeping server's identity and allowing clients to keep established connections. Key location:\n"
|
|
|
|
|
<> caPrivateKeyFile
|
|
|
|
|
<> caKeyFile
|
|
|
|
|
<> "\n----------"
|
|
|
|
|
|
|
|
|
|
deleteServer :: IO ()
|
|
|
|
|
deleteServer = do
|
|
|
|
|
ini <- runExceptT readIni
|
|
|
|
|
deleteIfExists iniFile
|
|
|
|
|
case ini of
|
|
|
|
|
-- TODO delete only cfgDir and logDir once file paths are removed from ini
|
|
|
|
|
Right IniOpts {storeLogFile, caCertificateFile, serverPrivateKeyFile, serverCertificateFile} -> do
|
|
|
|
|
deleteDirIfExists cfgDir
|
|
|
|
|
deleteIfExists storeLogFile
|
|
|
|
|
deleteIfExists caCertificateFile
|
|
|
|
|
deleteIfExists serverPrivateKeyFile
|
|
|
|
|
deleteIfExists serverCertificateFile
|
|
|
|
|
Left _ -> do
|
|
|
|
|
deleteDirIfExists cfgDir
|
|
|
|
|
deleteIfExists defaultStoreLogFile
|
|
|
|
|
deleteIfExists defaultCACertificateFile
|
|
|
|
|
deleteIfExists defaultPrivateKeyFile
|
|
|
|
|
deleteIfExists defaultCertificateFile
|
|
|
|
|
|
|
|
|
|
data IniOpts = IniOpts
|
|
|
|
|
{ enableStoreLog :: Bool,
|
|
|
|
|
storeLogFile :: FilePath,
|
|
|
|
|
serverPort :: ServiceName,
|
|
|
|
|
enableWebsockets :: Bool,
|
|
|
|
|
caCertificateFile :: FilePath,
|
|
|
|
|
serverPrivateKeyFile :: FilePath,
|
|
|
|
|
serverCertificateFile :: FilePath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
readIni :: ExceptT String IO IniOpts
|
|
|
|
|
readIni = do
|
|
|
|
|
fileExists iniFile
|
|
|
|
|
ini <- ExceptT $ readIniFile iniFile
|
|
|
|
|
let enableStoreLog = (== Right "on") $ lookupValue "STORE_LOG" "enable" ini
|
|
|
|
|
storeLogFile = opt defaultStoreLogFile "STORE_LOG" "file" ini
|
|
|
|
|
serverPort = opt defaultServerPort "TRANSPORT" "port" ini
|
|
|
|
|
enableWebsockets = (== Right "on") $ lookupValue "TRANSPORT" "websockets" ini
|
|
|
|
|
caCertificateFile = opt defaultCACertificateFile "TRANSPORT" "ca_certificate_file" ini
|
|
|
|
|
serverPrivateKeyFile = opt defaultPrivateKeyFile "TRANSPORT" "private_key_file" ini
|
|
|
|
|
serverCertificateFile = opt defaultCertificateFile "TRANSPORT" "certificate_file" ini
|
|
|
|
|
pure IniOpts {enableStoreLog, storeLogFile, serverPort, enableWebsockets, caCertificateFile, serverPrivateKeyFile, serverCertificateFile}
|
|
|
|
|
where
|
|
|
|
|
opt :: String -> Text -> Text -> Ini -> String
|
|
|
|
|
opt def section key ini = either (const def) T.unpack $ lookupValue section key ini
|
|
|
|
|
|
|
|
|
|
createIni :: ServerOpts -> IO IniOpts
|
|
|
|
|
createIni ServerOpts {enableStoreLog} = do
|
|
|
|
|
writeFile iniFile $
|
|
|
|
|
"[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"
|
|
|
|
|
<> (if enableStoreLog then "" else "# ")
|
|
|
|
|
<> "enable: on\n\
|
|
|
|
|
\# file: "
|
|
|
|
|
<> defaultStoreLogFile
|
|
|
|
|
<> "\n\n\
|
|
|
|
|
\[TRANSPORT]\n\n\
|
|
|
|
|
\# ca_certificate_file: "
|
|
|
|
|
<> defaultCACertificateFile
|
|
|
|
|
<> "\n\
|
|
|
|
|
\# private_key_file: "
|
|
|
|
|
<> defaultPrivateKeyFile
|
|
|
|
|
<> "\n\
|
|
|
|
|
\# certificate_file: "
|
|
|
|
|
<> defaultCertificateFile
|
|
|
|
|
<> "\n\
|
|
|
|
|
\# port: "
|
|
|
|
|
<> defaultServerPort
|
|
|
|
|
<> "\n\
|
|
|
|
|
\websockets: on\n"
|
|
|
|
|
pure
|
|
|
|
|
IniOpts
|
|
|
|
|
{ enableStoreLog,
|
|
|
|
|
storeLogFile = defaultStoreLogFile,
|
|
|
|
|
serverPort = defaultServerPort,
|
|
|
|
|
enableWebsockets = True,
|
|
|
|
|
caCertificateFile = defaultCACertificateFile,
|
|
|
|
|
serverPrivateKeyFile = defaultPrivateKeyFile,
|
|
|
|
|
serverCertificateFile = defaultCertificateFile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createX509 :: IniOpts -> ServerOpts -> IO ()
|
|
|
|
|
createX509 IniOpts {caCertificateFile, serverPrivateKeyFile, serverCertificateFile} ServerOpts {pubkeyAlgorihtm} = do
|
|
|
|
|
createOpensslConf
|
|
|
|
|
-- CA certificate (identity/offline)
|
|
|
|
|
run $ "openssl genpkey -algorithm " <> pubkeyAlgorihtm <> " -out " <> caPrivateKeyFile
|
|
|
|
|
run $ "openssl req -new -x509 -days 999999 -config " <> opensslConfFile <> " -extensions v3_ca -key " <> caPrivateKeyFile <> " -out " <> caCertificateFile
|
|
|
|
|
-- server certificate (online)
|
|
|
|
|
run $ "openssl genpkey -algorithm " <> pubkeyAlgorihtm <> " -out " <> serverPrivateKeyFile
|
|
|
|
|
run $ "openssl req -new -config " <> opensslConfFile <> " -reqexts v3_req -key " <> serverPrivateKeyFile <> " -out " <> serverCsrFile
|
|
|
|
|
run $ "openssl x509 -req -days 999999 -copy_extensions copy -in " <> serverCsrFile <> " -CA " <> caCertificateFile <> " -CAkey " <> caPrivateKeyFile <> " -out " <> serverCertificateFile
|
|
|
|
|
where
|
|
|
|
|
run cmd = void $ readCreateProcess (shell cmd) ""
|
|
|
|
|
opensslConfFile = combine cfgDir "openssl.cnf"
|
|
|
|
|
serverCsrFile = combine cfgDir "server.csr"
|
|
|
|
|
createOpensslConf :: IO ()
|
|
|
|
|
createOpensslConf =
|
|
|
|
|
-- TODO revise https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3, https://www.rfc-editor.org/rfc/rfc3279#section-2.3.5
|
|
|
|
|
writeFile
|
|
|
|
|
opensslConfFile
|
|
|
|
|
"[req]\n\
|
|
|
|
|
\distinguished_name = req_distinguished_name\n\
|
|
|
|
|
\prompt = no\n\n\
|
|
|
|
|
\[req_distinguished_name]\n\
|
|
|
|
|
\CN = localhost\n\n\
|
|
|
|
|
\[v3_ca]\n\
|
|
|
|
|
\subjectKeyIdentifier = hash\n\
|
|
|
|
|
\authorityKeyIdentifier = keyid:always\n\
|
|
|
|
|
\basicConstraints = critical,CA:true\n\n\
|
|
|
|
|
\[v3_req]\n\
|
|
|
|
|
\basicConstraints = CA:FALSE\n\
|
|
|
|
|
\keyUsage = digitalSignature, nonRepudiation, keyAgreement\n\
|
|
|
|
|
\extendedKeyUsage = serverAuth\n"
|
|
|
|
|
|
|
|
|
|
saveFingerprint :: FilePath -> IO ()
|
|
|
|
|
saveFingerprint caCertificateFile = do
|
|
|
|
|
fingerprint <- loadFingerprint caCertificateFile
|
|
|
|
|
writeFile fingerprintFile $ (B.unpack . encodeFingerprint) fingerprint <> "\n"
|
|
|
|
|
|
|
|
|
|
loadSavedFingerprint :: IO String
|
|
|
|
|
loadSavedFingerprint = do
|
|
|
|
|
fingerpint <- readFile fingerprintFile
|
|
|
|
|
pure $ dropWhileEnd (== '\n') fingerpint
|
|
|
|
|
|
|
|
|
|
fileExists :: FilePath -> ExceptT String IO ()
|
|
|
|
|
fileExists path = do
|
|
|
|
|
exists <- liftIO $ doesFileExist path
|
|
|
|
|
unless exists . throwE $ "file " <> path <> " not found"
|
|
|
|
|
|
|
|
|
|
deleteIfExists :: FilePath -> IO ()
|
|
|
|
|
deleteIfExists path = doesFileExist path >>= (`when` removeFile path)
|
|
|
|
|
|
|
|
|
|
deleteDirIfExists :: FilePath -> IO ()
|
|
|
|
|
deleteDirIfExists path = doesDirectoryExist path >>= (`when` removeDirectoryRecursive path)
|
|
|
|
|
|
|
|
|
|
confirm :: String -> IO ()
|
|
|
|
|
confirm msg = do
|
|
|
|
|
putStr $ msg <> " (y/N): "
|
|
|
|
|
hFlush stdout
|
|
|
|
|
ok <- getLine
|
|
|
|
|
when (map toLower ok /= "y") exitFailure
|
|
|
|
|
|
|
|
|
|
openStoreLog :: ServerOpts -> IniOpts -> IO (Maybe (StoreLog 'ReadMode))
|
|
|
|
|
openStoreLog ServerOpts {enableStoreLog = l} IniOpts {enableStoreLog = l', storeLogFile = f}
|
|
|
|
|
| l || l' = do
|
|
|
|
|
createDirectoryIfMissing True logDir
|
|
|
|
|
Just <$> openReadStoreLog f
|
|
|
|
|
| otherwise = pure Nothing
|
|
|
|
|
|
|
|
|
|
data ServerOpts = ServerOpts
|
|
|
|
|
{ serverCommand :: ServerCommand,
|
|
|
|
|
enableStoreLog :: Bool,
|
|
|
|
|
pubkeyAlgorihtm :: String
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data ServerCommand = ServerInit | ServerStart | ServerDelete
|
|
|
|
|
|
|
|
|
|
serverOpts :: Parser ServerOpts
|
|
|
|
|
serverOpts =
|
|
|
|
|
ServerOpts
|
|
|
|
|
<$> subparser
|
|
|
|
|
( command "init" (info (pure ServerInit) (progDesc "Initialize server: generate server key and ini file"))
|
|
|
|
|
<> command "start" (info (pure ServerStart) (progDesc "Start server (ini: /etc/opt/simplex/smp-server.ini)"))
|
|
|
|
|
<> command "delete" (info (pure ServerDelete) (progDesc "Delete server key, ini file and store log"))
|
|
|
|
|
)
|
|
|
|
|
<*> switch
|
|
|
|
|
( long "store-log"
|
|
|
|
|
<> short 'l'
|
|
|
|
|
<> help "enable store log for SMP queues persistence"
|
|
|
|
|
)
|
|
|
|
|
<*> strOption
|
|
|
|
|
( long "pubkey-algorithm"
|
|
|
|
|
<> short 'a'
|
|
|
|
|
<> help
|
|
|
|
|
( "public-key algorithm used for certificate generation,"
|
|
|
|
|
<> "\nsupported algorithms: ED448 (default) and ED25519"
|
|
|
|
|
)
|
|
|
|
|
<> value "ED448"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
getServerOpts :: IO ServerOpts
|
|
|
|
|
getServerOpts = customExecParser p opts
|
|
|
|
|
where
|
|
|
|
|
p = prefs showHelpOnEmpty
|
|
|
|
|
opts =
|
|
|
|
|
info
|
|
|
|
|
(serverOpts <**> helper)
|
|
|
|
|
( fullDesc
|
|
|
|
|
<> header "Simplex Messaging Protocol (SMP) Server"
|
|
|
|
|
)
|
|
|
|
|
fingerprint <- readFile fingerprintFile
|
|
|
|
|
pure $ dropWhileEnd (== '\n') fingerprint
|
|
|
|
|