Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion simplexmq.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ library
, directory ==1.3.*
, filepath ==1.4.*
, hourglass ==0.2.*
, http-client ==0.7.*
, http-types ==0.12.*
, http2 >=4.2.2 && <4.3
, iproute ==1.7.*
Expand Down Expand Up @@ -343,7 +344,6 @@ library
case-insensitive ==1.2.*
, hashable ==1.4.*
, ini ==0.4.1
, http-client ==0.7.*
, http-client-tls ==0.3.6.*
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
Expand Down
33 changes: 33 additions & 0 deletions src/Simplex/Messaging/Agent.hs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ module Simplex.Messaging.Agent
reconnectSMPServer,
registerNtfToken,
verifyNtfToken,
verifySavedNtfToken,
checkNtfToken,
deleteNtfToken,
deleteSavedNtfToken,
getNtfToken,
getNtfTokenData,
toggleConnectionNtfs,
Expand Down Expand Up @@ -628,6 +630,11 @@ verifyNtfToken :: AgentClient -> NetworkRequestMode -> DeviceToken -> C.CbNonce
verifyNtfToken c = withAgentEnv c .:: verifyNtfToken' c
{-# INLINE verifyNtfToken #-}

-- | Verify saved device notifications token
verifySavedNtfToken :: AgentClient -> NetworkRequestMode -> ByteString -> AE ()
verifySavedNtfToken c = withAgentEnv c .: verifySavedNtfToken' c
{-# INLINE verifySavedNtfToken #-}

checkNtfToken :: AgentClient -> NetworkRequestMode -> DeviceToken -> AE NtfTknStatus
checkNtfToken c = withAgentEnv c .: checkNtfToken' c
{-# INLINE checkNtfToken #-}
Expand All @@ -636,6 +643,10 @@ deleteNtfToken :: AgentClient -> DeviceToken -> AE ()
deleteNtfToken c = withAgentEnv c . deleteNtfToken' c
{-# INLINE deleteNtfToken #-}

deleteSavedNtfToken :: AgentClient -> AE ()
deleteSavedNtfToken c = withAgentEnv c $ deleteSavedNtfToken' c
{-# INLINE deleteSavedNtfToken #-}

getNtfToken :: AgentClient -> AE (DeviceToken, NtfTknStatus, NotificationsMode, NtfServer)
getNtfToken c = withAgentEnv c $ getNtfToken' c
{-# INLINE getNtfToken #-}
Expand Down Expand Up @@ -2544,6 +2555,19 @@ verifyNtfToken' c nm deviceToken nonce code =
when (ntfMode == NMInstant) $ initializeNtfSubs c
_ -> throwE $ CMD PROHIBITED "verifyNtfToken: no token"

verifySavedNtfToken' :: AgentClient -> NetworkRequestMode -> ByteString -> AM ()
verifySavedNtfToken' c nm code =
withStore' c getSavedNtfToken >>= \case
Just tkn@NtfToken {ntfTokenId = Just tknId, ntfMode} -> do
let code' = NtfRegCode code
toStatus <-
withToken c nm tkn (Just (NTConfirmed, NTAVerify code')) (NTActive, Just NTACheck) $
agentNtfVerifyToken c nm tknId tkn code'
when (toStatus == NTActive) $ do
lift $ setCronInterval c nm tknId tkn
when (ntfMode == NMInstant) $ initializeNtfSubs c
_ -> throwE $ CMD PROHIBITED "verifySavedNtfToken: no token"

setCronInterval :: AgentClient -> NetworkRequestMode -> NtfTokenId -> NtfToken -> AM' ()
setCronInterval c nm tknId tkn = do
cron <- asks $ ntfCron . config
Expand Down Expand Up @@ -2572,6 +2596,15 @@ deleteNtfToken' c deviceToken =
deleteNtfSubs c NSCSmpDelete
_ -> throwE $ CMD PROHIBITED "deleteNtfToken: no token"


deleteSavedNtfToken' :: AgentClient -> AM ()
deleteSavedNtfToken' c =
withStore' c getSavedNtfToken >>= \case
Just tkn -> do
deleteToken c tkn
deleteNtfSubs c NSCSmpDelete
_ -> throwE $ CMD PROHIBITED "deleteSavedNtfToken: no token"

getNtfToken' :: AgentClient -> AM (DeviceToken, NtfTknStatus, NotificationsMode, NtfServer)
getNtfToken' c =
withStore' c getSavedNtfToken >>= \case
Expand Down
1 change: 1 addition & 0 deletions src/Simplex/Messaging/Crypto.hs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ module Simplex.Messaging.Crypto
encodePrivKey,
decodePrivKey,
pubKeyBytes,
encodeBigInt,
uncompressEncodePoint,
uncompressDecodePoint,
uncompressDecodePrivateNumber,
Expand Down
3 changes: 3 additions & 0 deletions src/Simplex/Messaging/Notifications/Protocol.hs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ newtype WPSrvLoc = WPSrvLoc SrvLoc
newtype WPProvider = WPP WPSrvLoc
deriving (Eq, Ord, Show)

wpAud :: WPProvider -> B.ByteString
wpAud (WPP (WPSrvLoc (SrvLoc aud _))) = B.pack aud

instance Encoding PushProvider where
smpEncode = \case
PPAPNS p -> smpEncode p
Expand Down
23 changes: 14 additions & 9 deletions src/Simplex/Messaging/Notifications/Server/Env.hs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ import Simplex.Messaging.Transport.Server (AddHTTP, ServerCredentials, Transport
import System.Exit (exitFailure)
import System.Mem.Weak (Weak)
import UnliftIO.STM
import Simplex.Messaging.Notifications.Server.Push.WebPush (wpPushProviderClient)
import Simplex.Messaging.Notifications.Server.Push.WebPush (wpPushProviderClient, WebPushConfig)
import Network.HTTP.Client (newManager, ManagerSettings (..), Request (..), Manager)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Data.IORef (newIORef)

data NtfServerConfig = NtfServerConfig
{ transports :: [(ServiceName, ASrvTransport, AddHTTP)],
Expand All @@ -61,6 +62,7 @@ data NtfServerConfig = NtfServerConfig
pushQSize :: Natural,
smpAgentCfg :: SMPClientAgentConfig,
apnsConfig :: APNSPushClientConfig,
wpConfig :: WebPushConfig,
subsBatchSize :: Int,
inactiveClientExpiration :: Maybe ExpirationConfig,
dbStoreConfig :: PostgresStoreCfg,
Expand Down Expand Up @@ -100,7 +102,7 @@ data NtfEnv = NtfEnv
}

newNtfServerEnv :: NtfServerConfig -> IO NtfEnv
newNtfServerEnv config@NtfServerConfig {pushQSize, smpAgentCfg, apnsConfig, dbStoreConfig, ntfCredentials, useServiceCreds, startOptions} = do
newNtfServerEnv config@NtfServerConfig {pushQSize, smpAgentCfg, apnsConfig, wpConfig, dbStoreConfig, ntfCredentials, useServiceCreds, startOptions} = do
when (compactLog startOptions) $ compactDbStoreLog $ dbStoreLogPath dbStoreConfig
random <- C.newRandom
store <- newNtfDbStore dbStoreConfig
Expand All @@ -116,7 +118,7 @@ newNtfServerEnv config@NtfServerConfig {pushQSize, smpAgentCfg, apnsConfig, dbSt
pure smpAgentCfg {smpCfg = (smpCfg smpAgentCfg) {serviceCredentials = Just service}}
else pure smpAgentCfg
subscriber <- newNtfSubscriber smpAgentCfg' random
pushServer <- newNtfPushServer pushQSize apnsConfig
pushServer <- newNtfPushServer pushQSize apnsConfig wpConfig
serverStats <- newNtfServerStats =<< getCurrentTime
pure NtfEnv {config, subscriber, pushServer, store, random, tlsServerCreds, serverIdentity = C.KeyHash fp, serverStats}
where
Expand Down Expand Up @@ -153,14 +155,15 @@ data SMPSubscriber = SMPSubscriber
data NtfPushServer = NtfPushServer
{ pushQ :: TBQueue (Maybe T.Text, NtfTknRec, PushNotification), -- Maybe Text is a hostname of "own" server
pushClients :: TMap PushProvider PushProviderClient,
apnsConfig :: APNSPushClientConfig
apnsConfig :: APNSPushClientConfig,
wpConfig :: WebPushConfig
}

newNtfPushServer :: Natural -> APNSPushClientConfig -> IO NtfPushServer
newNtfPushServer qSize apnsConfig = do
newNtfPushServer :: Natural -> APNSPushClientConfig -> WebPushConfig -> IO NtfPushServer
newNtfPushServer qSize apnsConfig wpConfig = do
pushQ <- newTBQueueIO qSize
pushClients <- TM.emptyIO
pure NtfPushServer {pushQ, pushClients, apnsConfig}
pure NtfPushServer {pushQ, pushClients, apnsConfig, wpConfig}

newPushClient :: NtfPushServer -> PushProvider -> IO PushProviderClient
newPushClient s pp = do
Expand All @@ -177,10 +180,12 @@ newAPNSPushClient NtfPushServer {apnsConfig, pushClients} pp = do
Just host -> apnsPushProviderClient <$> createAPNSPushClient host apnsConfig

newWPPushClient :: NtfPushServer -> WPProvider -> IO PushProviderClient
newWPPushClient NtfPushServer {pushClients} pp = do
newWPPushClient NtfPushServer {wpConfig, pushClients} pp = do
logDebug "New WP Client requested"
-- We use one http manager per push server (which may be used by different clients)
wpPushProviderClient <$> wpHTTPManager
manager <- wpHTTPManager
cache <- newIORef Nothing
pure $ wpPushProviderClient wpConfig cache manager

wpHTTPManager :: IO Manager
wpHTTPManager = newManager tlsManagerSettings {
Expand Down
29 changes: 26 additions & 3 deletions src/Simplex/Messaging/Notifications/Server/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
module Simplex.Messaging.Notifications.Server.Main where

import Control.Logger.Simple (setLogLevel)
import Control.Monad ((<$!>))
import Control.Monad ( (<$!>), unless, void )
import qualified Data.ByteString.Char8 as B
import Data.Functor (($>))
import Data.Ini (lookupValue, readIniFile)
Expand Down Expand Up @@ -56,6 +56,8 @@ import System.Exit (exitFailure)
import System.FilePath (combine)
import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
import Text.Read (readMaybe)
import System.Process (readCreateProcess, shell)
import Simplex.Messaging.Notifications.Server.Push.WebPush (WebPushConfig(..), VapidKey(..), mkVapid)

ntfServerCLI :: FilePath -> FilePath -> IO ()
ntfServerCLI cfgPath logPath =
Expand Down Expand Up @@ -146,6 +148,7 @@ ntfServerCLI cfgPath logPath =
clearDirIfExists logPath
createDirectoryIfMissing True cfgPath
createDirectoryIfMissing True logPath
_ <- genVapidKey vapidKeyPath
let x509cfg = defaultX509Config {commonName = fromMaybe ip fqdn, signAlgorithm}
fp <- createServerX509 cfgPath x509cfg
let host = fromMaybe (if ip == "127.0.0.1" then "<hostnames>" else ip) fqdn
Expand Down Expand Up @@ -212,11 +215,13 @@ ntfServerCLI cfgPath logPath =
hSetBuffering stdout LineBuffering
hSetBuffering stderr LineBuffering
fp <- checkSavedFingerprint cfgPath defaultX509Config
vapidKey@VapidKey {fp = vapidFp } <- getVapidKey vapidKeyPath
let host = either (const "<hostnames>") T.unpack $ lookupValue "TRANSPORT" "host" ini
port = T.unpack $ strictIni "TRANSPORT" "port" ini
cfg@NtfServerConfig {transports} = serverConfig
cfg@NtfServerConfig {transports} = serverConfig vapidKey
srv = ProtoServerWithAuth (NtfServer [THDomainName host] (if port == "443" then "" else port) (C.KeyHash fp)) Nothing
printServiceInfo serverVersion srv
B.putStrLn $ "VAPID: " <> vapidFp
printNtfServerConfig transports dbStoreConfig
runNtfServer cfg
where
Expand All @@ -230,7 +235,7 @@ ntfServerCLI cfgPath logPath =
confirmMigrations = MCYesUp,
deletedTTL = iniDeletedTTL ini
}
serverConfig =
serverConfig vapidKey =
NtfServerConfig
{ transports = iniTransports ini,
controlPort = either (const Nothing) (Just . T.unpack) $ lookupValue "TRANSPORT" "control_port" ini,
Expand Down Expand Up @@ -258,6 +263,7 @@ ntfServerCLI cfgPath logPath =
persistErrorInterval = 0 -- seconds
},
apnsConfig = defaultAPNSPushClientConfig,
wpConfig = WebPushConfig {vapidKey},
subsBatchSize = 900,
inactiveClientExpiration =
settingIsOn "INACTIVE_CLIENTS" "disconnect" ini
Expand Down Expand Up @@ -294,6 +300,7 @@ ntfServerCLI cfgPath logPath =
putStrLn $ "Error: both " <> storeLogFilePath <> " file and " <> B.unpack schema <> " schema are present (database: " <> B.unpack connstr <> ")."
putStrLn "Configure notification server storage."
exitFailure
vapidKeyPath = combine cfgPath "vapid.privkey"

printNtfServerConfig :: [(ServiceName, ASrvTransport, AddHTTP)] -> PostgresStoreCfg -> IO ()
printNtfServerConfig transports PostgresStoreCfg {dbOpts = DBOpts {connstr, schema}, dbStoreLogPath} = do
Expand Down Expand Up @@ -395,3 +402,19 @@ cliCommandP cfgPath logPath iniFile =
<> metavar "FQDN"
)
pure InitOptions {enableStoreLog, dbOptions, signAlgorithm, ip, fqdn}

genVapidKey :: FilePath -> IO VapidKey
genVapidKey file = do
cfgExists <- doesFileExist file
unless cfgExists $ run $ "openssl ecparam -name prime256v1 -genkey -noout -out " <> file
key <- C.readECPrivateKey file
pure $ mkVapid key
where
run cmd = void $ readCreateProcess (shell cmd) ""

getVapidKey :: FilePath -> IO VapidKey
getVapidKey file = do
cfgExists <- doesFileExist file
unless cfgExists $ error $ "VAPID key not found: " <> file
key <- C.readECPrivateKey file
pure $ mkVapid key
45 changes: 40 additions & 5 deletions src/Simplex/Messaging/Notifications/Server/Push.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module Simplex.Messaging.Notifications.Server.Push where

import Crypto.Hash.Algorithms (SHA256 (..))
import qualified Crypto.PubKey.ECC.ECDSA as EC
import qualified Crypto.PubKey.ECC.Types as ECT
import qualified Crypto.Store.PKCS8 as PK
import Data.ASN1.BinaryEncoding (DER (..))
import Data.ASN1.Encoding
import Data.ASN1.Types
Expand All @@ -25,6 +27,7 @@ import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import Data.Time.Clock.System
import qualified Data.X509 as X
import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2ClientError)
Expand All @@ -36,14 +39,21 @@ import Control.Monad.Except (ExceptT)
import GHC.Exception (SomeException)

data JWTHeader = JWTHeader
{ alg :: Text, -- key algorithm, ES256 for APNS
kid :: Text -- key ID
{ typ :: Text, -- "JWT"
alg :: Text, -- key algorithm, ES256 for APNS
kid :: Maybe Text -- key ID
}
deriving (Show)

mkJWTHeader :: Text -> Maybe Text -> JWTHeader
mkJWTHeader alg kid = JWTHeader { typ = "JWT", alg, kid }

data JWTClaims = JWTClaims
{ iss :: Text, -- issuer, team ID for APNS
iat :: Int64 -- issue time, seconds from epoch
{ iss :: Maybe Text, -- issuer, team ID for APNS
iat :: Maybe Int64, -- issue time, seconds from epoch for APNS
exp :: Maybe Int64, -- expired time, seconds from epoch for web push
aud :: Maybe Text, -- audience, for web push
sub :: Maybe Text -- subject, to be inform if there is an issue, for web push
}
deriving (Show)

Expand All @@ -53,7 +63,15 @@ data JWTToken = JWTToken JWTHeader JWTClaims
mkJWTToken :: JWTHeader -> Text -> IO JWTToken
mkJWTToken hdr iss = do
iat <- systemSeconds <$> getSystemTime
pure $ JWTToken hdr JWTClaims {iss, iat}
pure $ JWTToken hdr $ jwtClaims iat
where
jwtClaims iat = JWTClaims
{ iss = Just iss,
iat = Just iat,
exp = Nothing,
aud = Nothing,
sub = Nothing
}

type SignedJWTToken = ByteString

Expand All @@ -71,6 +89,23 @@ signedJWTToken pk (JWTToken hdr claims) = do
jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode
serialize sig = U.encodeUnpadded $ encodeASN1' DER [Start Sequence, IntVal (EC.sign_r sig), IntVal (EC.sign_s sig), End Sequence]

-- | Does it work with APNS ?
signedJWTTokenRawSign :: EC.PrivateKey -> JWTToken -> IO SignedJWTToken
signedJWTTokenRawSign pk (JWTToken hdr claims) = do
let hc = jwtEncode hdr <> "." <> jwtEncode claims
sig <- EC.sign pk SHA256 hc
pure $ hc <> "." <> serialize sig
where
jwtEncode :: ToJSON a => a -> ByteString
jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode
serialize sig = U.encodeUnpadded $ LB.toStrict $ C.encodeBigInt (EC.sign_r sig) <> C.encodeBigInt (EC.sign_s sig)

readECPrivateKey :: FilePath -> IO EC.PrivateKey
readECPrivateKey f = do
-- this pattern match is specific to APNS key type, it may need to be extended for other push providers
[PK.Unprotected (X.PrivKeyEC X.PrivKeyEC_Named {privkeyEC_name, privkeyEC_priv})] <- PK.readKeyFile f
pure EC.PrivateKey {private_curve = ECT.getCurveByName privkeyEC_name, private_d = privkeyEC_priv}

data PushNotification
= PNVerification NtfRegCode
| PNMessage (NonEmpty PNMessageData)
Expand Down
5 changes: 3 additions & 2 deletions src/Simplex/Messaging/Notifications/Server/Push/APNS.hs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ createAPNSPushClient apnsHost apnsCfg@APNSPushClientConfig {authKeyFileEnv, auth
void $ connectHTTPS2 apnsHost apnsCfg https2Client
privateKey <- C.readECPrivateKey =<< getEnv authKeyFileEnv
authKeyId <- T.pack <$> getEnv authKeyIdEnv
let jwtHeader = JWTHeader {alg = authKeyAlg, kid = authKeyId}
let jwtHeader = mkJWTHeader authKeyAlg (Just authKeyId)
jwtToken <- newTVarIO =<< mkApnsJWTToken appTeamId jwtHeader privateKey
nonceDrg <- C.newRandom
pure APNSPushClient {https2Client, privateKey, jwtHeader, jwtToken, nonceDrg, apnsHost, apnsCfg}
Expand All @@ -178,7 +178,8 @@ getApnsJWTToken APNSPushClient {apnsCfg = APNSPushClientConfig {appTeamId, token
atomically $ writeTVar jwtToken t
pure signedJWT'
where
jwtTokenAge (JWTToken _ JWTClaims {iat}) = subtract iat . systemSeconds <$> getSystemTime
jwtTokenAge (JWTToken _ JWTClaims {iat = Just iat}) = subtract iat . systemSeconds <$> getSystemTime
jwtTokenAge (JWTToken _ JWTClaims {iat = Nothing}) = pure maxBound :: IO Int64

mkApnsJWTToken :: Text -> JWTHeader -> EC.PrivateKey -> IO (JWTToken, SignedJWTToken)
mkApnsJWTToken appTeamId jwtHeader privateKey = do
Expand Down
Loading
Loading