2023-10-11 11:45:05 +03:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
2023-09-27 11:41:02 +03:00
|
|
|
{-# LANGUAGE DuplicateRecordFields #-}
|
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
2023-10-13 20:53:04 +03:00
|
|
|
{-# LANGUAGE GADTs #-}
|
2023-09-27 11:41:02 +03:00
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
2023-11-08 22:13:52 +02:00
|
|
|
{-# LANGUAGE OverloadedLists #-}
|
2023-09-27 11:41:02 +03:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2023-10-07 16:23:24 +03:00
|
|
|
{-# LANGUAGE TupleSections #-}
|
2023-11-08 22:13:52 +02:00
|
|
|
{-# LANGUAGE TypeApplications #-}
|
2023-10-13 17:52:27 +01:00
|
|
|
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
|
|
|
|
|
2023-09-27 11:41:02 +03:00
|
|
|
module Simplex.Chat.Remote where
|
|
|
|
|
2023-10-30 16:00:54 +02:00
|
|
|
import Control.Applicative ((<|>))
|
2023-10-07 16:23:24 +03:00
|
|
|
import Control.Logger.Simple
|
2023-10-04 18:36:10 +03:00
|
|
|
import Control.Monad
|
2023-09-27 11:41:02 +03:00
|
|
|
import Control.Monad.Except
|
|
|
|
import Control.Monad.IO.Class
|
2023-10-29 19:06:32 +00:00
|
|
|
import Control.Monad.Reader
|
2023-10-04 18:36:10 +03:00
|
|
|
import Crypto.Random (getRandomBytes)
|
2023-09-27 11:41:02 +03:00
|
|
|
import qualified Data.Aeson as J
|
2023-11-08 22:13:52 +02:00
|
|
|
import qualified Data.Aeson.Types as JT
|
|
|
|
import Data.Bifunctor (second)
|
2023-10-22 11:42:19 +03:00
|
|
|
import Data.ByteString (ByteString)
|
2023-10-04 18:36:10 +03:00
|
|
|
import qualified Data.ByteString.Base64.URL as B64U
|
2023-10-22 11:42:19 +03:00
|
|
|
import Data.ByteString.Builder (Builder)
|
2023-10-04 18:36:10 +03:00
|
|
|
import qualified Data.ByteString.Char8 as B
|
2023-10-30 16:00:54 +02:00
|
|
|
import Data.Functor (($>))
|
2023-09-27 11:41:02 +03:00
|
|
|
import qualified Data.Map.Strict as M
|
2023-11-08 22:13:52 +02:00
|
|
|
import Data.Maybe (fromMaybe, isNothing)
|
2023-10-15 00:18:04 +01:00
|
|
|
import Data.Text (Text)
|
2023-10-07 16:23:24 +03:00
|
|
|
import qualified Data.Text as T
|
2023-11-08 22:13:52 +02:00
|
|
|
import Data.Text.Encoding (encodeUtf8)
|
2023-10-30 16:00:54 +02:00
|
|
|
import Data.Word (Word16, Word32)
|
2023-10-22 11:42:19 +03:00
|
|
|
import qualified Network.HTTP.Types as N
|
2023-11-08 22:13:52 +02:00
|
|
|
import Network.HTTP2.Client (HTTP2Error (..))
|
2023-10-30 16:00:54 +02:00
|
|
|
import Network.HTTP2.Server (responseStreaming)
|
2023-11-08 22:13:52 +02:00
|
|
|
import qualified Paths_simplex_chat as SC
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Chat.Archive (archiveFilesFolder)
|
2023-09-27 11:41:02 +03:00
|
|
|
import Simplex.Chat.Controller
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Chat.Files
|
|
|
|
import Simplex.Chat.Messages (chatNameStr)
|
2023-11-08 22:13:52 +02:00
|
|
|
import Simplex.Chat.Remote.AppVersion
|
2023-10-22 11:42:19 +03:00
|
|
|
import Simplex.Chat.Remote.Protocol
|
2023-11-08 22:13:52 +02:00
|
|
|
import Simplex.Chat.Remote.RevHTTP (attachHTTP2Server, attachRevHTTP2Client)
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Chat.Remote.Transport
|
2023-09-27 11:41:02 +03:00
|
|
|
import Simplex.Chat.Remote.Types
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Chat.Store.Files
|
2023-09-29 14:56:56 +03:00
|
|
|
import Simplex.Chat.Store.Remote
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Chat.Store.Shared
|
2023-11-08 22:13:52 +02:00
|
|
|
import Simplex.Chat.Types
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Chat.Util (encryptFile)
|
|
|
|
import Simplex.FileTransfer.Description (FileDigest (..))
|
2023-11-08 22:13:52 +02:00
|
|
|
import Simplex.Messaging.Agent
|
|
|
|
import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP))
|
2023-09-29 14:56:56 +03:00
|
|
|
import qualified Simplex.Messaging.Crypto as C
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
|
|
|
import qualified Simplex.Messaging.Crypto.File as CF
|
2023-09-29 14:56:56 +03:00
|
|
|
import Simplex.Messaging.Encoding.String (StrEncoding (..))
|
2023-10-30 16:00:54 +02:00
|
|
|
import qualified Simplex.Messaging.TMap as TM
|
2023-09-29 14:56:56 +03:00
|
|
|
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
2023-11-08 22:13:52 +02:00
|
|
|
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2ClientError, closeHTTP2Client)
|
2023-10-22 11:42:19 +03:00
|
|
|
import Simplex.Messaging.Transport.HTTP2.File (hSendFile)
|
2023-10-29 19:06:32 +00:00
|
|
|
import Simplex.Messaging.Transport.HTTP2.Server (HTTP2Request (..))
|
2023-11-08 22:13:52 +02:00
|
|
|
import Simplex.Messaging.Util
|
|
|
|
import Simplex.RemoteControl.Client
|
|
|
|
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
|
2023-11-01 12:48:58 +02:00
|
|
|
import Simplex.RemoteControl.Types
|
2023-10-30 16:00:54 +02:00
|
|
|
import System.FilePath (takeFileName, (</>))
|
2023-09-29 14:56:56 +03:00
|
|
|
import UnliftIO
|
2023-11-08 22:13:52 +02:00
|
|
|
import UnliftIO.Concurrent (forkIO)
|
2023-10-29 19:06:32 +00:00
|
|
|
import UnliftIO.Directory (copyFile, createDirectoryIfMissing, renameFile)
|
2023-10-22 11:42:19 +03:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
-- when acting as host
|
|
|
|
minRemoteCtrlVersion :: AppVersion
|
|
|
|
minRemoteCtrlVersion = AppVersion [5, 4, 0, 2]
|
2023-09-27 11:41:02 +03:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
-- when acting as controller
|
|
|
|
minRemoteHostVersion :: AppVersion
|
|
|
|
minRemoteHostVersion = AppVersion [5, 4, 0, 2]
|
2023-10-22 11:42:19 +03:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
currentAppVersion :: AppVersion
|
|
|
|
currentAppVersion = AppVersion SC.version
|
|
|
|
|
|
|
|
ctrlAppVersionRange :: AppVersionRange
|
|
|
|
ctrlAppVersionRange = mkAppVersionRange minRemoteHostVersion currentAppVersion
|
|
|
|
|
|
|
|
hostAppVersionRange :: AppVersionRange
|
|
|
|
hostAppVersionRange = mkAppVersionRange minRemoteCtrlVersion currentAppVersion
|
2023-10-15 14:17:36 +01:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
-- * Desktop side
|
|
|
|
|
|
|
|
getRemoteHostClient :: ChatMonad m => RemoteHostId -> m RemoteHostClient
|
|
|
|
getRemoteHostClient rhId = withRemoteHostSession rhKey $ \case
|
|
|
|
s@RHSessionConnected {rhClient} -> Right (rhClient, s)
|
|
|
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
2023-10-15 14:17:36 +01:00
|
|
|
where
|
2023-11-08 22:13:52 +02:00
|
|
|
rhKey = RHId rhId
|
2023-10-22 11:42:19 +03:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
withRemoteHostSession :: ChatMonad m => RHKey -> (RemoteHostSession -> Either ChatError (a, RemoteHostSession)) -> m a
|
|
|
|
withRemoteHostSession rhKey state = withRemoteHostSession_ rhKey $ maybe (Left $ ChatErrorRemoteHost rhKey $ RHEMissing) ((second . second) Just . state)
|
|
|
|
|
|
|
|
withRemoteHostSession_ :: ChatMonad m => RHKey -> (Maybe RemoteHostSession -> Either ChatError (a, Maybe RemoteHostSession)) -> m a
|
|
|
|
withRemoteHostSession_ rhKey state = do
|
|
|
|
sessions <- asks remoteHostSessions
|
|
|
|
r <- atomically $ do
|
|
|
|
s <- TM.lookup rhKey sessions
|
|
|
|
case state s of
|
|
|
|
Left e -> pure $ Left e
|
|
|
|
Right (a, s') -> Right a <$ maybe (TM.delete rhKey) (TM.insert rhKey) s' sessions
|
|
|
|
liftEither r
|
|
|
|
|
|
|
|
setNewRemoteHostId :: ChatMonad m => RHKey -> RemoteHostId -> m ()
|
|
|
|
setNewRemoteHostId rhKey rhId = do
|
2023-10-22 11:42:19 +03:00
|
|
|
sessions <- asks remoteHostSessions
|
2023-11-08 22:13:52 +02:00
|
|
|
r <- atomically $ do
|
|
|
|
TM.lookupDelete rhKey sessions >>= \case
|
|
|
|
Nothing -> pure $ Left $ ChatErrorRemoteHost rhKey RHEMissing
|
|
|
|
Just s -> Right () <$ TM.insert (RHId rhId) s sessions
|
|
|
|
liftEither r
|
|
|
|
|
|
|
|
startRemoteHost' :: ChatMonad m => Maybe (RemoteHostId, Bool) -> m (Maybe RemoteHostInfo, RCSignedInvitation)
|
|
|
|
startRemoteHost' rh_ = do
|
|
|
|
(rhKey, multicast, remoteHost_, pairing) <- case rh_ of
|
|
|
|
Just (rhId, multicast) -> do
|
|
|
|
rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId
|
|
|
|
pure (RHId rhId, multicast, Just $ remoteHostInfo rh True, hostPairing) -- get from the database, start multicast if requested
|
|
|
|
Nothing -> (RHNew,False,Nothing,) <$> rcNewHostPairing
|
|
|
|
withRemoteHostSession_ rhKey $ maybe (Right ((), Just RHSessionStarting)) (\_ -> Left $ ChatErrorRemoteHost rhKey RHEBusy)
|
|
|
|
ctrlAppInfo <- mkCtrlAppInfo
|
|
|
|
(invitation, rchClient, vars) <- withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast
|
2023-11-09 20:25:05 +02:00
|
|
|
cmdOk <- newEmptyTMVarIO
|
|
|
|
rhsWaitSession <- async $ do
|
|
|
|
atomically $ takeTMVar cmdOk
|
|
|
|
cleanupOnError rchClient $ waitForSession remoteHost_ vars
|
2023-11-08 22:13:52 +02:00
|
|
|
let rhs = RHPendingSession {rhKey, rchClient, rhsWaitSession, remoteHost_}
|
|
|
|
withRemoteHostSession rhKey $ \case
|
|
|
|
RHSessionStarting -> Right ((), RHSessionConnecting rhs)
|
|
|
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
2023-11-09 20:25:05 +02:00
|
|
|
(remoteHost_, invitation) <$ atomically (putTMVar cmdOk ())
|
2023-10-07 16:23:24 +03:00
|
|
|
where
|
2023-11-08 22:13:52 +02:00
|
|
|
mkCtrlAppInfo = do
|
|
|
|
deviceName <- chatReadVar localDeviceName
|
|
|
|
pure CtrlAppInfo {appVersionRange = ctrlAppVersionRange, deviceName}
|
2023-11-09 09:37:56 +00:00
|
|
|
parseHostAppInfo :: RCHostHello -> ExceptT RemoteHostError IO HostAppInfo
|
|
|
|
parseHostAppInfo RCHostHello {app = hostAppInfo} = do
|
2023-11-09 20:25:05 +02:00
|
|
|
hostInfo@HostAppInfo {appVersion, encoding} <-
|
2023-11-09 09:37:56 +00:00
|
|
|
liftEitherWith (RHEProtocolError . RPEInvalidJSON) $ JT.parseEither J.parseJSON hostAppInfo
|
|
|
|
unless (isAppCompatible appVersion ctrlAppVersionRange) $ throwError $ RHEBadVersion appVersion
|
|
|
|
when (encoding == PEKotlin && localEncoding == PESwift) $ throwError $ RHEProtocolError RPEIncompatibleEncoding
|
|
|
|
pure hostInfo
|
2023-11-09 20:25:05 +02:00
|
|
|
cleanupOnError :: ChatMonad m => RCHostClient -> (TMVar RHKey -> m ()) -> m ()
|
|
|
|
cleanupOnError rchClient action = do
|
|
|
|
currentKey <- newEmptyTMVarIO
|
|
|
|
action currentKey `catchChatError` \err -> do
|
|
|
|
logError $ "startRemoteHost'.waitForSession crashed: " <> tshow err
|
|
|
|
sessions <- asks remoteHostSessions
|
|
|
|
atomically $ readTMVar currentKey >>= (`TM.delete` sessions)
|
|
|
|
liftIO $ cancelHostClient rchClient
|
|
|
|
waitForSession :: ChatMonad m => Maybe RemoteHostInfo -> RCStepTMVar (ByteString, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> TMVar RHKey -> m ()
|
|
|
|
waitForSession remoteHost_ vars currentKey = do
|
|
|
|
let rhKey = maybe RHNew (\RemoteHostInfo {remoteHostId} -> RHId remoteHostId) remoteHost_
|
|
|
|
atomically $ writeTMVar currentKey rhKey
|
2023-11-08 22:13:52 +02:00
|
|
|
(sessId, vars') <- takeRCStep vars
|
|
|
|
toView $ CRRemoteHostSessionCode {remoteHost_, sessionCode = verificationCode sessId} -- display confirmation code, wait for mobile to confirm
|
|
|
|
(RCHostSession {tls, sessionKeys}, rhHello, pairing') <- takeRCStep vars'
|
2023-11-09 09:37:56 +00:00
|
|
|
hostInfo@HostAppInfo {deviceName = hostDeviceName} <-
|
|
|
|
liftError (ChatErrorRemoteHost rhKey) $ parseHostAppInfo rhHello
|
2023-11-08 22:13:52 +02:00
|
|
|
withRemoteHostSession rhKey $ \case
|
|
|
|
RHSessionConnecting rhs' -> Right ((), RHSessionConfirmed rhs') -- TODO check it's the same session?
|
2023-11-09 20:25:05 +02:00
|
|
|
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
|
2023-11-08 22:13:52 +02:00
|
|
|
-- update remoteHost with updated pairing
|
|
|
|
rhi@RemoteHostInfo {remoteHostId, storePath} <- upsertRemoteHost pairing' remoteHost_ hostDeviceName
|
2023-11-09 20:25:05 +02:00
|
|
|
let rhKey' = RHId remoteHostId -- rhKey may be invalid after upserting on RHNew
|
|
|
|
atomically $ writeTMVar currentKey rhKey'
|
2023-11-08 22:13:52 +02:00
|
|
|
disconnected <- toIO $ onDisconnected remoteHostId
|
2023-11-09 20:25:05 +02:00
|
|
|
httpClient <- liftEitherError (httpError rhKey') $ attachRevHTTP2Client disconnected tls
|
2023-11-09 09:37:56 +00:00
|
|
|
let rhClient = mkRemoteHostClient httpClient sessionKeys storePath hostInfo
|
2023-11-08 22:13:52 +02:00
|
|
|
pollAction <- async $ pollEvents remoteHostId rhClient
|
|
|
|
withRemoteHostSession rhKey' $ \case
|
|
|
|
RHSessionConfirmed RHPendingSession {} -> Right ((), RHSessionConnected {rhClient, pollAction, storePath})
|
2023-11-09 20:25:05 +02:00
|
|
|
_ -> Left $ ChatErrorRemoteHost rhKey' RHEBadState
|
2023-11-08 22:13:52 +02:00
|
|
|
chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host
|
|
|
|
toView $ CRRemoteHostConnected rhi
|
|
|
|
upsertRemoteHost :: ChatMonad m => RCHostPairing -> Maybe RemoteHostInfo -> Text -> m RemoteHostInfo
|
2023-11-09 20:25:05 +02:00
|
|
|
upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ hostDeviceName = do
|
2023-11-08 22:13:52 +02:00
|
|
|
KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_
|
2023-11-09 20:25:05 +02:00
|
|
|
case rhi_ of
|
2023-11-08 22:13:52 +02:00
|
|
|
Nothing -> do
|
|
|
|
storePath <- liftIO randomStorePath
|
|
|
|
rh@RemoteHost {remoteHostId} <- withStore $ \db -> insertRemoteHost db hostDeviceName storePath pairing' >>= getRemoteHost db
|
|
|
|
setNewRemoteHostId RHNew remoteHostId
|
|
|
|
pure $ remoteHostInfo rh True
|
|
|
|
Just rhi@RemoteHostInfo {remoteHostId} -> do
|
|
|
|
withStore' $ \db -> updateHostPairing db remoteHostId hostDeviceName hostDhPubKey'
|
|
|
|
pure rhi
|
|
|
|
onDisconnected :: ChatMonad m => RemoteHostId -> m ()
|
|
|
|
onDisconnected remoteHostId = do
|
|
|
|
logDebug "HTTP2 client disconnected"
|
|
|
|
chatModifyVar currentRemoteHost $ \cur -> if cur == Just remoteHostId then Nothing else cur -- only wipe the closing RH
|
|
|
|
sessions <- asks remoteHostSessions
|
|
|
|
void . atomically $ TM.lookupDelete (RHId remoteHostId) sessions
|
|
|
|
toView $ CRRemoteHostStopped remoteHostId
|
|
|
|
pollEvents :: ChatMonad m => RemoteHostId -> RemoteHostClient -> m ()
|
|
|
|
pollEvents rhId rhClient = do
|
2023-10-15 14:17:36 +01:00
|
|
|
oq <- asks outputQ
|
2023-11-08 22:13:52 +02:00
|
|
|
forever $ do
|
|
|
|
r_ <- liftRH rhId $ remoteRecv rhClient 10000000
|
|
|
|
forM r_ $ \r -> atomically $ writeTBQueue oq (Nothing, Just rhId, r)
|
|
|
|
httpError :: RHKey -> HTTP2ClientError -> ChatError
|
|
|
|
httpError rhKey = ChatErrorRemoteHost rhKey . RHEProtocolError . RPEHTTP2 . tshow
|
|
|
|
|
|
|
|
closeRemoteHost :: ChatMonad m => RHKey -> m ()
|
|
|
|
closeRemoteHost rhKey = do
|
|
|
|
logNote $ "Closing remote host session for " <> tshow rhKey
|
|
|
|
chatModifyVar currentRemoteHost $ \cur -> if (RHId <$> cur) == Just rhKey then Nothing else cur -- only wipe the closing RH
|
|
|
|
join . withRemoteHostSession_ rhKey . maybe (Left $ ChatErrorRemoteCtrl RCEInactive) $
|
|
|
|
\s -> Right (liftIO $ cancelRemoteHost s, Nothing)
|
|
|
|
|
|
|
|
cancelRemoteHost :: RemoteHostSession -> IO ()
|
|
|
|
cancelRemoteHost = \case
|
|
|
|
RHSessionStarting -> pure ()
|
|
|
|
RHSessionConnecting rhs -> cancelPendingSession rhs
|
|
|
|
RHSessionConfirmed rhs -> cancelPendingSession rhs
|
|
|
|
RHSessionConnected {rhClient = RemoteHostClient {httpClient}, pollAction} -> do
|
|
|
|
uninterruptibleCancel pollAction
|
|
|
|
closeHTTP2Client httpClient
|
|
|
|
where
|
|
|
|
cancelPendingSession RHPendingSession {rchClient, rhsWaitSession} = do
|
|
|
|
cancelHostClient rchClient
|
|
|
|
uninterruptibleCancel rhsWaitSession
|
2023-10-04 18:36:10 +03:00
|
|
|
|
|
|
|
-- | Generate a random 16-char filepath without / in it by using base64url encoding.
|
|
|
|
randomStorePath :: IO FilePath
|
|
|
|
randomStorePath = B.unpack . B64U.encode <$> getRandomBytes 12
|
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
listRemoteHosts :: ChatMonad m => m [RemoteHostInfo]
|
2023-10-04 18:36:10 +03:00
|
|
|
listRemoteHosts = do
|
|
|
|
active <- chatReadVar remoteHostSessions
|
2023-10-30 16:00:54 +02:00
|
|
|
map (rhInfo active) <$> withStore' getRemoteHosts
|
2023-10-14 13:10:06 +01:00
|
|
|
where
|
2023-10-30 16:00:54 +02:00
|
|
|
rhInfo active rh@RemoteHost {remoteHostId} =
|
2023-11-08 22:13:52 +02:00
|
|
|
remoteHostInfo rh (M.member (RHId remoteHostId) active)
|
2023-10-15 00:18:04 +01:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
-- XXX: replacing hostPairing replaced with sessionActive, could be a ($>)
|
2023-10-30 16:00:54 +02:00
|
|
|
remoteHostInfo :: RemoteHost -> Bool -> RemoteHostInfo
|
2023-11-08 22:13:52 +02:00
|
|
|
remoteHostInfo RemoteHost {remoteHostId, storePath, hostName} sessionActive =
|
|
|
|
RemoteHostInfo {remoteHostId, storePath, hostName, sessionActive}
|
2023-10-04 18:36:10 +03:00
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
deleteRemoteHost :: ChatMonad m => RemoteHostId -> m ()
|
|
|
|
deleteRemoteHost rhId = do
|
|
|
|
RemoteHost {storePath} <- withStore (`getRemoteHost` rhId)
|
2023-10-13 20:53:04 +03:00
|
|
|
chatReadVar filesFolder >>= \case
|
|
|
|
Just baseDir -> do
|
|
|
|
let hostStore = baseDir </> storePath
|
|
|
|
logError $ "TODO: remove " <> tshow hostStore
|
|
|
|
Nothing -> logWarn "Local file store not available while deleting remote host"
|
2023-10-15 14:17:36 +01:00
|
|
|
withStore' (`deleteRemoteHostRecord` rhId)
|
|
|
|
|
2023-10-29 19:06:32 +00:00
|
|
|
storeRemoteFile :: forall m. ChatMonad m => RemoteHostId -> Maybe Bool -> FilePath -> m CryptoFile
|
|
|
|
storeRemoteFile rhId encrypted_ localPath = do
|
2023-11-08 22:13:52 +02:00
|
|
|
c@RemoteHostClient {encryptHostFiles, storePath} <- getRemoteHostClient rhId
|
|
|
|
let encrypt = fromMaybe encryptHostFiles encrypted_
|
|
|
|
cf@CryptoFile {filePath} <- if encrypt then encryptLocalFile else pure $ CF.plain localPath
|
|
|
|
filePath' <- liftRH rhId $ remoteStoreFile c filePath (takeFileName localPath)
|
|
|
|
hf_ <- chatReadVar remoteHostsFolder
|
|
|
|
forM_ hf_ $ \hf -> do
|
|
|
|
let rhf = hf </> storePath </> archiveFilesFolder
|
|
|
|
hPath = rhf </> takeFileName filePath'
|
|
|
|
createDirectoryIfMissing True rhf
|
|
|
|
(if encrypt then renameFile else copyFile) filePath hPath
|
|
|
|
pure (cf :: CryptoFile) {filePath = filePath'}
|
2023-10-29 19:06:32 +00:00
|
|
|
where
|
|
|
|
encryptLocalFile :: m CryptoFile
|
|
|
|
encryptLocalFile = do
|
|
|
|
tmpDir <- getChatTempDirectory
|
|
|
|
createDirectoryIfMissing True tmpDir
|
|
|
|
tmpFile <- tmpDir `uniqueCombine` takeFileName localPath
|
|
|
|
cfArgs <- liftIO CF.randomArgs
|
|
|
|
liftError (ChatError . CEFileWrite tmpFile) $ encryptFile localPath tmpFile cfArgs
|
|
|
|
pure $ CryptoFile tmpFile $ Just cfArgs
|
|
|
|
|
|
|
|
getRemoteFile :: ChatMonad m => RemoteHostId -> RemoteFile -> m ()
|
|
|
|
getRemoteFile rhId rf = do
|
2023-11-08 22:13:52 +02:00
|
|
|
c@RemoteHostClient {storePath} <- getRemoteHostClient rhId
|
|
|
|
dir <- (</> storePath </> archiveFilesFolder) <$> (maybe getDefaultFilesFolder pure =<< chatReadVar remoteHostsFolder)
|
|
|
|
createDirectoryIfMissing True dir
|
|
|
|
liftRH rhId $ remoteGetFile c dir rf
|
|
|
|
|
|
|
|
processRemoteCommand :: ChatMonad m => RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> m ChatResponse
|
|
|
|
processRemoteCommand remoteHostId c cmd s = case cmd of
|
2023-10-29 19:06:32 +00:00
|
|
|
SendFile chatName f -> sendFile "/f" chatName f
|
2023-10-30 16:00:54 +02:00
|
|
|
SendImage chatName f -> sendFile "/img" chatName f
|
2023-11-08 22:13:52 +02:00
|
|
|
_ -> liftRH remoteHostId $ remoteSend c s
|
2023-10-29 19:06:32 +00:00
|
|
|
where
|
|
|
|
sendFile cmdName chatName (CryptoFile path cfArgs) = do
|
|
|
|
-- don't encrypt in host if already encrypted locally
|
|
|
|
CryptoFile path' cfArgs' <- storeRemoteFile remoteHostId (cfArgs $> False) path
|
|
|
|
let f = CryptoFile path' (cfArgs <|> cfArgs') -- use local or host encryption
|
2023-11-08 22:13:52 +02:00
|
|
|
liftRH remoteHostId $ remoteSend c $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f]
|
2023-10-29 19:06:32 +00:00
|
|
|
cryptoFileStr CryptoFile {filePath, cryptoArgs} =
|
|
|
|
maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs
|
|
|
|
<> encodeUtf8 (T.pack filePath)
|
2023-10-07 16:23:24 +03:00
|
|
|
|
2023-10-22 11:42:19 +03:00
|
|
|
liftRH :: ChatMonad m => RemoteHostId -> ExceptT RemoteProtocolError IO a -> m a
|
2023-11-08 22:13:52 +02:00
|
|
|
liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError)
|
2023-09-29 14:56:56 +03:00
|
|
|
|
2023-10-22 11:42:19 +03:00
|
|
|
-- * Mobile side
|
2023-09-29 14:56:56 +03:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
findKnownRemoteCtrl :: ChatMonad m => m ()
|
|
|
|
findKnownRemoteCtrl = undefined -- do
|
|
|
|
|
|
|
|
-- | Use provided OOB link as an annouce
|
|
|
|
connectRemoteCtrl :: ChatMonad m => RCSignedInvitation -> m ()
|
|
|
|
connectRemoteCtrl inv@RCSignedInvitation {invitation = RCInvitation {ca, app}} = do
|
|
|
|
(ctrlDeviceName, v) <- parseCtrlAppInfo app
|
|
|
|
withRemoteCtrlSession_ $ maybe (Right ((), Just RCSessionStarting)) (\_ -> Left $ ChatErrorRemoteCtrl RCEBusy)
|
|
|
|
rc_ <- withStore' $ \db -> getRemoteCtrlByFingerprint db ca
|
|
|
|
hostAppInfo <- getHostAppInfo v
|
|
|
|
(rcsClient, vars) <- withAgent $ \a -> rcConnectCtrlURI a inv (ctrlPairing <$> rc_) (J.toJSON hostAppInfo)
|
2023-11-09 20:25:05 +02:00
|
|
|
cmdOk <- newEmptyTMVarIO
|
|
|
|
rcsWaitSession <- async $ do
|
|
|
|
atomically $ takeTMVar cmdOk
|
|
|
|
cleanupOnError rcsClient $ waitForSession rc_ ctrlDeviceName rcsClient vars
|
|
|
|
cleanupOnError rcsClient . updateRemoteCtrlSession $ \case
|
2023-11-08 22:13:52 +02:00
|
|
|
RCSessionStarting -> Right RCSessionConnecting {rcsClient, rcsWaitSession}
|
2023-11-09 20:25:05 +02:00
|
|
|
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
|
|
|
|
atomically $ putTMVar cmdOk ()
|
2023-11-08 22:13:52 +02:00
|
|
|
where
|
2023-11-09 20:25:05 +02:00
|
|
|
cleanupOnError :: ChatMonad m => RCCtrlClient -> m () -> m ()
|
|
|
|
cleanupOnError rcsClient action = action `catchChatError` \e -> do
|
|
|
|
logError $ "connectRemoteCtrl crashed with: " <> tshow e
|
|
|
|
chatWriteVar remoteCtrlSession Nothing -- XXX: can only wipe PendingConfirmation or RCSessionConnecting, which only have rcsClient to cancel
|
|
|
|
liftIO $ cancelCtrlClient rcsClient
|
2023-11-08 22:13:52 +02:00
|
|
|
waitForSession :: ChatMonad m => Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> m ()
|
|
|
|
waitForSession rc_ ctrlName rcsClient vars = do
|
|
|
|
(uniq, rcsWaitConfirmation) <- takeRCStep vars
|
|
|
|
let sessionCode = verificationCode uniq
|
|
|
|
toView CRRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` True) <$> rc_, sessionCode}
|
|
|
|
updateRemoteCtrlSession $ \case
|
|
|
|
RCSessionConnecting {rcsWaitSession} -> Right RCSessionPendingConfirmation {ctrlName, rcsClient, sessionCode, rcsWaitSession, rcsWaitConfirmation}
|
2023-11-09 20:25:05 +02:00
|
|
|
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
|
2023-11-08 22:13:52 +02:00
|
|
|
parseCtrlAppInfo ctrlAppInfo = do
|
|
|
|
CtrlAppInfo {deviceName, appVersionRange} <-
|
|
|
|
liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo
|
|
|
|
v <- case compatibleAppVersion hostAppVersionRange appVersionRange of
|
|
|
|
Just (AppCompatible v) -> pure v
|
|
|
|
Nothing -> throwError $ ChatErrorRemoteCtrl $ RCEBadVersion $ maxVersion appVersionRange
|
|
|
|
pure (deviceName, v)
|
|
|
|
getHostAppInfo appVersion = do
|
|
|
|
hostDeviceName <- chatReadVar localDeviceName
|
|
|
|
encryptFiles <- chatReadVar encryptLocalFiles
|
|
|
|
pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles}
|
|
|
|
|
|
|
|
handleRemoteCommand :: forall m. ChatMonad m => (ByteString -> m ChatResponse) -> CtrlSessKeys -> TBQueue ChatResponse -> HTTP2Request -> m ()
|
|
|
|
handleRemoteCommand execChatCommand _sessionKeys remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
|
2023-10-22 11:42:19 +03:00
|
|
|
logDebug "handleRemoteCommand"
|
|
|
|
liftRC (tryRemoteError parseRequest) >>= \case
|
2023-10-29 19:06:32 +00:00
|
|
|
Right (getNext, rc) -> do
|
|
|
|
chatReadVar currentUser >>= \case
|
|
|
|
Nothing -> replyError $ ChatError CENoActiveUser
|
|
|
|
Just user -> processCommand user getNext rc `catchChatError` replyError
|
2023-10-22 11:42:19 +03:00
|
|
|
Left e -> reply $ RRProtocolError e
|
2023-10-15 14:17:36 +01:00
|
|
|
where
|
2023-10-22 11:42:19 +03:00
|
|
|
parseRequest :: ExceptT RemoteProtocolError IO (GetChunk, RemoteCommand)
|
|
|
|
parseRequest = do
|
|
|
|
(header, getNext) <- parseHTTP2Body request reqBody
|
2023-11-08 22:13:52 +02:00
|
|
|
(getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecodeStrict' header)
|
2023-10-29 19:06:32 +00:00
|
|
|
replyError = reply . RRChatResponse . CRChatCmdError Nothing
|
|
|
|
processCommand :: User -> GetChunk -> RemoteCommand -> m ()
|
|
|
|
processCommand user getNext = \case
|
2023-10-22 11:42:19 +03:00
|
|
|
RCSend {command} -> handleSend execChatCommand command >>= reply
|
|
|
|
RCRecv {wait = time} -> handleRecv time remoteOutputQ >>= reply
|
2023-10-29 19:06:32 +00:00
|
|
|
RCStoreFile {fileName, fileSize, fileDigest} -> handleStoreFile fileName fileSize fileDigest getNext >>= reply
|
|
|
|
RCGetFile {file} -> handleGetFile user file replyWith
|
2023-10-22 11:42:19 +03:00
|
|
|
reply :: RemoteResponse -> m ()
|
|
|
|
reply = (`replyWith` \_ -> pure ())
|
|
|
|
replyWith :: Respond m
|
|
|
|
replyWith rr attach =
|
|
|
|
liftIO . sendResponse . responseStreaming N.status200 [] $ \send flush -> do
|
|
|
|
send $ sizePrefixedEncode rr
|
|
|
|
attach send
|
|
|
|
flush
|
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
takeRCStep :: ChatMonad m => RCStepTMVar a -> m a
|
|
|
|
takeRCStep = liftEitherError (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar
|
|
|
|
|
2023-10-22 11:42:19 +03:00
|
|
|
type GetChunk = Int -> IO ByteString
|
|
|
|
|
|
|
|
type SendChunk = Builder -> IO ()
|
|
|
|
|
|
|
|
type Respond m = RemoteResponse -> (SendChunk -> IO ()) -> m ()
|
|
|
|
|
|
|
|
liftRC :: ChatMonad m => ExceptT RemoteProtocolError IO a -> m a
|
|
|
|
liftRC = liftError (ChatErrorRemoteCtrl . RCEProtocolError)
|
|
|
|
|
|
|
|
tryRemoteError :: ExceptT RemoteProtocolError IO a -> ExceptT RemoteProtocolError IO (Either RemoteProtocolError a)
|
|
|
|
tryRemoteError = tryAllErrors (RPEException . tshow)
|
|
|
|
{-# INLINE tryRemoteError #-}
|
|
|
|
|
|
|
|
handleSend :: ChatMonad m => (ByteString -> m ChatResponse) -> Text -> m RemoteResponse
|
|
|
|
handleSend execChatCommand command = do
|
|
|
|
logDebug $ "Send: " <> tshow command
|
|
|
|
-- execChatCommand checks for remote-allowed commands
|
|
|
|
-- convert errors thrown in ChatMonad into error responses to prevent aborting the protocol wrapper
|
|
|
|
RRChatResponse <$> execChatCommand (encodeUtf8 command) `catchError` (pure . CRChatError Nothing)
|
|
|
|
|
|
|
|
handleRecv :: MonadUnliftIO m => Int -> TBQueue ChatResponse -> m RemoteResponse
|
|
|
|
handleRecv time events = do
|
|
|
|
logDebug $ "Recv: " <> tshow time
|
|
|
|
RRChatEvent <$> (timeout time . atomically $ readTBQueue events)
|
|
|
|
|
2023-10-29 19:06:32 +00:00
|
|
|
-- TODO this command could remember stored files and return IDs to allow removing files that are not needed.
|
|
|
|
-- Also, there should be some process removing unused files uploaded to remote host (possibly, all unused files).
|
|
|
|
handleStoreFile :: forall m. ChatMonad m => FilePath -> Word32 -> FileDigest -> GetChunk -> m RemoteResponse
|
|
|
|
handleStoreFile fileName fileSize fileDigest getChunk =
|
|
|
|
either RRProtocolError RRFileStored <$> (chatReadVar filesFolder >>= storeFile)
|
|
|
|
where
|
|
|
|
storeFile :: Maybe FilePath -> m (Either RemoteProtocolError FilePath)
|
|
|
|
storeFile = \case
|
|
|
|
Just ff -> takeFileName <$$> storeFileTo ff
|
|
|
|
Nothing -> storeFileTo =<< getDefaultFilesFolder
|
|
|
|
storeFileTo :: FilePath -> m (Either RemoteProtocolError FilePath)
|
|
|
|
storeFileTo dir = liftRC . tryRemoteError $ do
|
|
|
|
filePath <- dir `uniqueCombine` fileName
|
|
|
|
receiveRemoteFile getChunk fileSize fileDigest filePath
|
|
|
|
pure filePath
|
|
|
|
|
|
|
|
handleGetFile :: ChatMonad m => User -> RemoteFile -> Respond m -> m ()
|
2023-11-01 19:08:36 +00:00
|
|
|
handleGetFile User {userId} RemoteFile {userId = commandUserId, fileId, sent, fileSource = cf'@CryptoFile {filePath}} reply = do
|
2023-10-29 19:06:32 +00:00
|
|
|
logDebug $ "GetFile: " <> tshow filePath
|
|
|
|
unless (userId == commandUserId) $ throwChatError $ CEDifferentActiveUser {commandUserId, activeUserId = userId}
|
|
|
|
path <- maybe filePath (</> filePath) <$> chatReadVar filesFolder
|
|
|
|
withStore $ \db -> do
|
|
|
|
cf <- getLocalCryptoFile db commandUserId fileId sent
|
|
|
|
unless (cf == cf') $ throwError $ SEFileNotFound fileId
|
|
|
|
liftRC (tryRemoteError $ getFileInfo path) >>= \case
|
|
|
|
Left e -> reply (RRProtocolError e) $ \_ -> pure ()
|
|
|
|
Right (fileSize, fileDigest) ->
|
|
|
|
withFile path ReadMode $ \h ->
|
|
|
|
reply RRFile {fileSize, fileDigest} $ \send -> hSendFile h send fileSize
|
2023-10-15 14:17:36 +01:00
|
|
|
|
2023-10-30 16:00:54 +02:00
|
|
|
discoverRemoteCtrls :: ChatMonad m => TM.TMap C.KeyHash (TransportHost, Word16) -> m ()
|
|
|
|
discoverRemoteCtrls discovered = do
|
2023-11-08 22:13:52 +02:00
|
|
|
error "TODO: discoverRemoteCtrls"
|
2023-10-15 14:17:36 +01:00
|
|
|
|
|
|
|
listRemoteCtrls :: ChatMonad m => m [RemoteCtrlInfo]
|
2023-10-04 18:36:10 +03:00
|
|
|
listRemoteCtrls = do
|
2023-11-08 22:13:52 +02:00
|
|
|
active <- chatReadVar remoteCtrlSession >>= \case
|
|
|
|
Just RCSessionConnected {remoteCtrlId} -> pure $ Just remoteCtrlId
|
|
|
|
_ -> pure Nothing
|
2023-10-14 13:10:06 +01:00
|
|
|
map (rcInfo active) <$> withStore' getRemoteCtrls
|
|
|
|
where
|
2023-10-15 14:17:36 +01:00
|
|
|
rcInfo activeRcId rc@RemoteCtrl {remoteCtrlId} =
|
|
|
|
remoteCtrlInfo rc $ activeRcId == Just remoteCtrlId
|
2023-10-15 00:18:04 +01:00
|
|
|
|
|
|
|
remoteCtrlInfo :: RemoteCtrl -> Bool -> RemoteCtrlInfo
|
2023-11-08 22:13:52 +02:00
|
|
|
remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlName} sessionActive =
|
|
|
|
RemoteCtrlInfo {remoteCtrlId, ctrlName, sessionActive}
|
2023-10-04 18:36:10 +03:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
-- XXX: only used for multicast
|
2023-11-01 19:08:36 +00:00
|
|
|
confirmRemoteCtrl :: ChatMonad m => RemoteCtrlId -> m ()
|
2023-11-08 22:13:52 +02:00
|
|
|
confirmRemoteCtrl _rcId = do
|
2023-10-15 14:17:36 +01:00
|
|
|
-- TODO check it exists, check the ID is the same as in session
|
2023-11-08 22:13:52 +02:00
|
|
|
-- RemoteCtrlSession {confirmed} <- getRemoteCtrlSession
|
|
|
|
-- withStore' $ \db -> markRemoteCtrlResolution db rcId True
|
|
|
|
-- atomically . void $ tryPutTMVar confirmed rcId -- the remote host can now proceed with connection
|
|
|
|
undefined
|
|
|
|
|
|
|
|
-- | Take a look at emoji of tlsunique, commit pairing, and start session server
|
|
|
|
verifyRemoteCtrlSession :: ChatMonad m => (ByteString -> m ChatResponse) -> Text -> m RemoteCtrlInfo
|
2023-11-09 20:25:05 +02:00
|
|
|
verifyRemoteCtrlSession execChatCommand sessCode' = cleanupOnError $ do
|
2023-11-08 22:13:52 +02:00
|
|
|
(client, ctrlName, sessionCode, vars) <-
|
|
|
|
getRemoteCtrlSession >>= \case
|
|
|
|
RCSessionPendingConfirmation {rcsClient, ctrlName, sessionCode, rcsWaitConfirmation} -> pure (rcsClient, ctrlName, sessionCode, rcsWaitConfirmation)
|
|
|
|
_ -> throwError $ ChatErrorRemoteCtrl RCEBadState
|
|
|
|
let verified = sameVerificationCode sessCode' sessionCode
|
|
|
|
liftIO $ confirmCtrlSession client verified
|
|
|
|
unless verified $ throwError $ ChatErrorRemoteCtrl RCEBadVerificationCode
|
|
|
|
(rcsSession@RCCtrlSession {tls, sessionKeys}, rcCtrlPairing) <- takeRCStep vars
|
2023-11-09 20:25:05 +02:00
|
|
|
rc@RemoteCtrl {remoteCtrlId} <- upsertRemoteCtrl ctrlName rcCtrlPairing
|
2023-11-08 22:13:52 +02:00
|
|
|
remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO
|
|
|
|
http2Server <- async $ attachHTTP2Server tls $ handleRemoteCommand execChatCommand sessionKeys remoteOutputQ
|
2023-11-09 20:25:05 +02:00
|
|
|
void . forkIO $ monitor http2Server
|
2023-11-08 22:13:52 +02:00
|
|
|
withRemoteCtrlSession $ \case
|
|
|
|
RCSessionPendingConfirmation {} -> Right ((), RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, http2Server, remoteOutputQ})
|
|
|
|
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
|
|
|
|
pure $ remoteCtrlInfo rc True
|
2023-11-09 20:25:05 +02:00
|
|
|
where
|
|
|
|
upsertRemoteCtrl :: ChatMonad m => Text -> RCCtrlPairing -> m RemoteCtrl
|
|
|
|
upsertRemoteCtrl ctrlName rcCtrlPairing = withStore $ \db -> do
|
|
|
|
rc_ <- liftIO $ getRemoteCtrlByFingerprint db (ctrlFingerprint rcCtrlPairing)
|
|
|
|
case rc_ of
|
|
|
|
Nothing -> insertRemoteCtrl db ctrlName rcCtrlPairing >>= getRemoteCtrl db
|
|
|
|
Just rc@RemoteCtrl {remoteCtrlId} -> do
|
|
|
|
liftIO $ updateCtrlPairingKeys db remoteCtrlId (dhPrivKey rcCtrlPairing)
|
|
|
|
pure rc
|
|
|
|
cleanupOnError :: ChatMonad m => m a -> m a
|
|
|
|
cleanupOnError action = action `catchChatError` \e -> do
|
|
|
|
logError $ "verifyRemoteCtrlSession crashed with: " <> tshow e
|
|
|
|
withRemoteCtrlSession_ (\s -> pure (s, Nothing)) >>= mapM_ (liftIO . cancelRemoteCtrl) -- cancel session threads, if any
|
|
|
|
throwError e
|
|
|
|
monitor :: ChatMonad m => Async a -> m ()
|
|
|
|
monitor server = do
|
|
|
|
waitCatch server >>= \case
|
|
|
|
Left err | Just (BadThingHappen innerErr) <- fromException err -> logWarn $ "HTTP2 server crashed with internal " <> tshow innerErr
|
|
|
|
Left err | isNothing (fromException @AsyncCancelled err) -> logError $ "HTTP2 server crashed with " <> tshow err
|
|
|
|
_ -> logInfo "HTTP2 server stopped"
|
|
|
|
toView CRRemoteCtrlStopped
|
2023-10-15 14:17:36 +01:00
|
|
|
|
|
|
|
stopRemoteCtrl :: ChatMonad m => m ()
|
2023-11-08 22:13:52 +02:00
|
|
|
stopRemoteCtrl =
|
|
|
|
join . withRemoteCtrlSession_ . maybe (Left $ ChatErrorRemoteCtrl RCEInactive) $
|
|
|
|
\s -> Right (liftIO $ cancelRemoteCtrl s, Nothing)
|
|
|
|
|
|
|
|
cancelRemoteCtrl :: RemoteCtrlSession -> IO ()
|
|
|
|
cancelRemoteCtrl = \case
|
|
|
|
RCSessionStarting -> pure ()
|
|
|
|
RCSessionConnecting {rcsClient, rcsWaitSession} -> do
|
|
|
|
cancelCtrlClient rcsClient
|
|
|
|
uninterruptibleCancel rcsWaitSession
|
|
|
|
RCSessionPendingConfirmation {rcsClient, rcsWaitSession} -> do
|
|
|
|
cancelCtrlClient rcsClient
|
|
|
|
uninterruptibleCancel rcsWaitSession
|
|
|
|
RCSessionConnected {rcsClient, http2Server} -> do
|
|
|
|
cancelCtrlClient rcsClient
|
|
|
|
uninterruptibleCancel http2Server
|
2023-10-07 16:23:24 +03:00
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
deleteRemoteCtrl :: ChatMonad m => RemoteCtrlId -> m ()
|
|
|
|
deleteRemoteCtrl rcId = do
|
|
|
|
checkNoRemoteCtrlSession
|
|
|
|
-- TODO check it exists
|
|
|
|
withStore' (`deleteRemoteCtrlRecord` rcId)
|
|
|
|
|
|
|
|
getRemoteCtrlSession :: ChatMonad m => m RemoteCtrlSession
|
|
|
|
getRemoteCtrlSession =
|
|
|
|
chatReadVar remoteCtrlSession >>= maybe (throwError $ ChatErrorRemoteCtrl RCEInactive) pure
|
|
|
|
|
|
|
|
checkNoRemoteCtrlSession :: ChatMonad m => m ()
|
|
|
|
checkNoRemoteCtrlSession =
|
|
|
|
chatReadVar remoteCtrlSession >>= maybe (pure ()) (\_ -> throwError $ ChatErrorRemoteCtrl RCEBusy)
|
2023-10-11 11:45:05 +03:00
|
|
|
|
2023-11-08 22:13:52 +02:00
|
|
|
withRemoteCtrlSession :: ChatMonad m => (RemoteCtrlSession -> Either ChatError (a, RemoteCtrlSession)) -> m a
|
|
|
|
withRemoteCtrlSession state = withRemoteCtrlSession_ $ maybe (Left $ ChatErrorRemoteCtrl RCEInactive) ((second . second) Just . state)
|
|
|
|
|
|
|
|
-- | Atomically process controller state wrt. specific remote ctrl session
|
|
|
|
withRemoteCtrlSession_ :: ChatMonad m => (Maybe RemoteCtrlSession -> Either ChatError (a, Maybe RemoteCtrlSession)) -> m a
|
|
|
|
withRemoteCtrlSession_ state = do
|
|
|
|
session <- asks remoteCtrlSession
|
|
|
|
r <-
|
|
|
|
atomically $ stateTVar session $ \s ->
|
|
|
|
case state s of
|
|
|
|
Left e -> (Left e, s)
|
|
|
|
Right (a, s') -> (Right a, s')
|
|
|
|
liftEither r
|
|
|
|
|
|
|
|
updateRemoteCtrlSession :: ChatMonad m => (RemoteCtrlSession -> Either ChatError RemoteCtrlSession) -> m ()
|
|
|
|
updateRemoteCtrlSession state = withRemoteCtrlSession $ fmap ((),) . state
|
|
|
|
|
2023-10-11 11:45:05 +03:00
|
|
|
utf8String :: [Char] -> ByteString
|
|
|
|
utf8String = encodeUtf8 . T.pack
|
|
|
|
{-# INLINE utf8String #-}
|