2023-11-01 19:08:36 +00:00
|
|
|
{-# LANGUAGE BlockArguments #-}
|
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 #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
2023-10-07 16:23:24 +03:00
|
|
|
{-# LANGUAGE TupleSections #-}
|
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 Control.Monad.STM (retry)
|
|
|
|
import Crypto.Random (getRandomBytes)
|
2023-09-27 11:41:02 +03:00
|
|
|
import qualified Data.Aeson as J
|
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-10-04 18:36:10 +03:00
|
|
|
import Data.List.NonEmpty (NonEmpty (..))
|
2023-09-27 11:41:02 +03:00
|
|
|
import qualified Data.Map.Strict as M
|
2023-10-11 11:45:05 +03:00
|
|
|
import Data.Maybe (fromMaybe)
|
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-10-30 16:00:54 +02:00
|
|
|
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
|
|
|
import Data.Word (Word16, Word32)
|
2023-10-22 11:42:19 +03:00
|
|
|
import qualified Network.HTTP.Types as N
|
2023-10-30 16:00:54 +02:00
|
|
|
import Network.HTTP2.Server (responseStreaming)
|
2023-09-29 14:56:56 +03:00
|
|
|
import Network.Socket (SockAddr (..), hostAddressToTuple)
|
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-10-22 11:42:19 +03:00
|
|
|
import Simplex.Chat.Remote.Protocol
|
2023-11-01 19:08:36 +00:00
|
|
|
import Simplex.Chat.Remote.RevHTTP (announceRevHTTP2, attachHTTP2Server)
|
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
|
|
|
|
import Simplex.Chat.Types (User (..))
|
|
|
|
import Simplex.Chat.Util (encryptFile)
|
|
|
|
import Simplex.FileTransfer.Description (FileDigest (..))
|
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-10-30 16:00:54 +02:00
|
|
|
import Simplex.Messaging.Encoding (smpDecode)
|
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-11-01 19:08:36 +00:00
|
|
|
import Simplex.Messaging.Transport (tlsUniq)
|
2023-09-29 14:56:56 +03:00
|
|
|
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
2023-10-04 18:36:10 +03:00
|
|
|
import Simplex.Messaging.Transport.Credentials (genCredentials, tlsCredentials)
|
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 (..))
|
|
|
|
import Simplex.Messaging.Util (ifM, liftEitherError, liftEitherWith, liftError, liftIOEither, tryAllErrors, tshow, ($>>=), (<$$>))
|
2023-11-01 12:48:58 +02:00
|
|
|
import qualified Simplex.RemoteControl.Discovery as Discovery
|
|
|
|
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-01 19:08:36 +00:00
|
|
|
import UnliftIO.Concurrent (threadDelay)
|
2023-10-29 19:06:32 +00:00
|
|
|
import UnliftIO.Directory (copyFile, createDirectoryIfMissing, renameFile)
|
2023-10-22 11:42:19 +03:00
|
|
|
|
|
|
|
-- * Desktop side
|
2023-09-27 11:41:02 +03:00
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
getRemoteHostSession :: ChatMonad m => RemoteHostId -> m RemoteHostSession
|
2023-10-22 11:42:19 +03:00
|
|
|
getRemoteHostSession rhId = withRemoteHostSession rhId $ \_ s -> pure $ Right s
|
|
|
|
|
|
|
|
withRemoteHostSession :: ChatMonad m => RemoteHostId -> (TM.TMap RemoteHostId RemoteHostSession -> RemoteHostSession -> STM (Either ChatError a)) -> m a
|
|
|
|
withRemoteHostSession rhId = withRemoteHostSession_ rhId missing
|
2023-09-27 11:41:02 +03:00
|
|
|
where
|
2023-10-22 11:42:19 +03:00
|
|
|
missing _ = pure . Left $ ChatErrorRemoteHost rhId RHMissing
|
2023-10-15 14:17:36 +01:00
|
|
|
|
2023-10-22 11:42:19 +03:00
|
|
|
withNoRemoteHostSession :: ChatMonad m => RemoteHostId -> (TM.TMap RemoteHostId RemoteHostSession -> STM (Either ChatError a)) -> m a
|
|
|
|
withNoRemoteHostSession rhId action = withRemoteHostSession_ rhId action busy
|
2023-10-15 14:17:36 +01:00
|
|
|
where
|
2023-10-22 11:42:19 +03:00
|
|
|
busy _ _ = pure . Left $ ChatErrorRemoteHost rhId RHBusy
|
|
|
|
|
|
|
|
-- | Atomically process controller state wrt. specific remote host session
|
|
|
|
withRemoteHostSession_ :: ChatMonad m => RemoteHostId -> (TM.TMap RemoteHostId RemoteHostSession -> STM (Either ChatError a)) -> (TM.TMap RemoteHostId RemoteHostSession -> RemoteHostSession -> STM (Either ChatError a)) -> m a
|
|
|
|
withRemoteHostSession_ rhId missing present = do
|
|
|
|
sessions <- asks remoteHostSessions
|
|
|
|
liftIOEither . atomically $ TM.lookup rhId sessions >>= maybe (missing sessions) (present sessions)
|
2023-10-15 14:17:36 +01:00
|
|
|
|
|
|
|
startRemoteHost :: ChatMonad m => RemoteHostId -> m ()
|
|
|
|
startRemoteHost rhId = do
|
|
|
|
rh <- withStore (`getRemoteHost` rhId)
|
2023-10-22 11:42:19 +03:00
|
|
|
tasks <- startRemoteHostSession rh
|
|
|
|
logInfo $ "Remote host session starting for " <> tshow rhId
|
2023-11-01 19:08:36 +00:00
|
|
|
asyncRegistered tasks $
|
|
|
|
run rh tasks `catchAny` \err -> do
|
|
|
|
logError $ "Remote host session startup failed for " <> tshow rhId <> ": " <> tshow err
|
|
|
|
cancelTasks tasks
|
|
|
|
chatModifyVar remoteHostSessions $ M.delete rhId
|
|
|
|
throwError $ fromMaybe (mkChatError err) $ fromException err
|
2023-10-07 16:23:24 +03:00
|
|
|
where
|
2023-11-01 19:08:36 +00:00
|
|
|
-- logInfo $ "Remote host session starting for " <> tshow rhId
|
|
|
|
|
2023-10-22 11:42:19 +03:00
|
|
|
run :: ChatMonad m => RemoteHost -> Tasks -> m ()
|
|
|
|
run rh@RemoteHost {storePath} tasks = do
|
|
|
|
(fingerprint, credentials) <- liftIO $ genSessionCredentials rh
|
|
|
|
cleanupIO <- toIO $ do
|
|
|
|
logNote $ "Remote host session stopping for " <> tshow rhId
|
|
|
|
cancelTasks tasks -- cancel our tasks anyway
|
|
|
|
chatModifyVar currentRemoteHost $ \cur -> if cur == Just rhId then Nothing else cur -- only wipe the closing RH
|
|
|
|
withRemoteHostSession rhId $ \sessions _ -> Right <$> TM.delete rhId sessions
|
|
|
|
toView (CRRemoteHostStopped rhId) -- only signal "stopped" when the session is unregistered cleanly
|
2023-11-01 19:08:36 +00:00
|
|
|
-- block until some client is connected or an error happens
|
2023-10-22 11:42:19 +03:00
|
|
|
logInfo $ "Remote host session connecting for " <> tshow rhId
|
2023-10-15 14:17:36 +01:00
|
|
|
rcName <- chatReadVar localDeviceName
|
2023-10-30 16:00:54 +02:00
|
|
|
localAddr <- asks multicastSubscribers >>= Discovery.getLocalAddress >>= maybe (throwError . ChatError $ CEInternalError "unable to get local address") pure
|
|
|
|
(dhKey, sigKey, ann, oob) <- Discovery.startSession (if rcName == "" then Nothing else Just rcName) (localAddr, read Discovery.DISCOVERY_PORT) fingerprint
|
|
|
|
toView CRRemoteHostStarted {remoteHost = remoteHostInfo rh True, sessionOOB = decodeUtf8 $ strEncode oob}
|
2023-11-01 12:48:58 +02:00
|
|
|
httpClient <- liftEitherError (ChatErrorRemoteCtrl . RCEHTTP2Error . show) $ announceRevHTTP2 tasks (sigKey, ann) credentials cleanupIO
|
2023-10-30 16:00:54 +02:00
|
|
|
logInfo $ "Remote host session connected for " <> tshow rhId
|
2023-10-22 11:42:19 +03:00
|
|
|
-- test connection and establish a protocol layer
|
2023-10-30 16:00:54 +02:00
|
|
|
remoteHostClient <- liftRH rhId $ createRemoteHostClient httpClient dhKey rcName
|
2023-10-22 11:42:19 +03:00
|
|
|
-- set up message polling
|
2023-10-15 14:17:36 +01:00
|
|
|
oq <- asks outputQ
|
2023-10-22 11:42:19 +03:00
|
|
|
asyncRegistered tasks . forever $ do
|
|
|
|
liftRH rhId (remoteRecv remoteHostClient 1000000) >>= mapM_ (atomically . writeTBQueue oq . (Nothing,Just rhId,))
|
|
|
|
-- update session state
|
|
|
|
logInfo $ "Remote host session started for " <> tshow rhId
|
|
|
|
chatModifyVar remoteHostSessions $ M.adjust (\rhs -> rhs {remoteHostClient = Just remoteHostClient}) rhId
|
|
|
|
chatWriteVar currentRemoteHost $ Just rhId
|
2023-11-01 19:08:36 +00:00
|
|
|
toView $
|
|
|
|
CRRemoteHostConnected
|
|
|
|
RemoteHostInfo
|
|
|
|
{ remoteHostId = rhId,
|
|
|
|
storePath = storePath,
|
|
|
|
displayName = hostDeviceName remoteHostClient,
|
|
|
|
sessionActive = True
|
|
|
|
}
|
2023-10-22 11:42:19 +03:00
|
|
|
|
|
|
|
genSessionCredentials RemoteHost {caKey, caCert} = do
|
|
|
|
sessionCreds <- genCredentials (Just parent) (0, 24) "Session"
|
|
|
|
pure . tlsCredentials $ sessionCreds :| [parent]
|
2023-10-15 14:17:36 +01:00
|
|
|
where
|
2023-10-22 11:42:19 +03:00
|
|
|
parent = (C.signatureKeyPair caKey, caCert)
|
2023-10-07 16:23:24 +03:00
|
|
|
|
2023-10-22 11:42:19 +03:00
|
|
|
-- | Atomically check/register session and prepare its task list
|
|
|
|
startRemoteHostSession :: ChatMonad m => RemoteHost -> m Tasks
|
|
|
|
startRemoteHostSession RemoteHost {remoteHostId, storePath} = withNoRemoteHostSession remoteHostId $ \sessions -> do
|
|
|
|
remoteHostTasks <- newTVar []
|
|
|
|
TM.insert remoteHostId RemoteHostSession {remoteHostTasks, storePath, remoteHostClient = Nothing} sessions
|
|
|
|
pure $ Right remoteHostTasks
|
2023-10-04 18:36:10 +03:00
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
closeRemoteHostSession :: ChatMonad m => RemoteHostId -> m ()
|
2023-10-22 11:42:19 +03:00
|
|
|
closeRemoteHostSession rhId = do
|
|
|
|
logNote $ "Closing remote host session for " <> tshow rhId
|
|
|
|
chatModifyVar currentRemoteHost $ \cur -> if cur == Just rhId then Nothing else cur -- only wipe the closing RH
|
|
|
|
session <- withRemoteHostSession rhId $ \sessions rhs -> Right rhs <$ TM.delete rhId sessions
|
|
|
|
cancelRemoteHostSession session
|
2023-10-07 16:23:24 +03:00
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
cancelRemoteHostSession :: MonadUnliftIO m => RemoteHostSession -> m ()
|
2023-10-22 11:42:19 +03:00
|
|
|
cancelRemoteHostSession RemoteHostSession {remoteHostTasks, remoteHostClient} = do
|
|
|
|
cancelTasks remoteHostTasks
|
|
|
|
mapM_ closeRemoteHostClient remoteHostClient
|
2023-10-04 18:36:10 +03:00
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
createRemoteHost :: ChatMonad m => m RemoteHostInfo
|
2023-10-04 18:36:10 +03:00
|
|
|
createRemoteHost = do
|
2023-10-22 11:42:19 +03:00
|
|
|
((_, caKey), caCert) <- liftIO $ genCredentials Nothing (-25, 24 * 365) "Host"
|
2023-10-04 18:36:10 +03:00
|
|
|
storePath <- liftIO randomStorePath
|
2023-10-22 11:42:19 +03:00
|
|
|
let remoteName = "" -- will be passed from remote host in hello
|
2023-10-30 16:00:54 +02:00
|
|
|
rhId <- withStore' $ \db -> insertRemoteHost db storePath remoteName caKey caCert
|
|
|
|
rh <- withStore $ \db -> getRemoteHost db rhId
|
|
|
|
pure $ remoteHostInfo rh False
|
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} =
|
|
|
|
remoteHostInfo rh (M.member remoteHostId active)
|
2023-10-15 00:18:04 +01:00
|
|
|
|
2023-10-30 16:00:54 +02:00
|
|
|
remoteHostInfo :: RemoteHost -> Bool -> RemoteHostInfo
|
|
|
|
remoteHostInfo RemoteHost {remoteHostId, storePath, displayName} sessionActive =
|
|
|
|
RemoteHostInfo {remoteHostId, storePath, displayName, 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
|
|
|
|
RemoteHostSession {remoteHostClient, storePath} <- getRemoteHostSession rhId
|
|
|
|
case remoteHostClient of
|
|
|
|
Nothing -> throwError $ ChatErrorRemoteHost rhId RHMissing
|
|
|
|
Just c@RemoteHostClient {encryptHostFiles} -> do
|
|
|
|
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'}
|
|
|
|
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
|
|
|
|
RemoteHostSession {remoteHostClient, storePath} <- getRemoteHostSession rhId
|
|
|
|
case remoteHostClient of
|
|
|
|
Nothing -> throwError $ ChatErrorRemoteHost rhId RHMissing
|
|
|
|
Just c -> do
|
|
|
|
dir <- (</> storePath </> archiveFilesFolder) <$> (maybe getDefaultFilesFolder pure =<< chatReadVar remoteHostsFolder)
|
|
|
|
createDirectoryIfMissing True dir
|
|
|
|
liftRH rhId $ remoteGetFile c dir rf
|
|
|
|
|
|
|
|
processRemoteCommand :: ChatMonad m => RemoteHostId -> RemoteHostSession -> ChatCommand -> ByteString -> m ChatResponse
|
|
|
|
processRemoteCommand remoteHostId RemoteHostSession {remoteHostClient = Just rhc} cmd s = case cmd of
|
|
|
|
SendFile chatName f -> sendFile "/f" chatName f
|
2023-10-30 16:00:54 +02:00
|
|
|
SendImage chatName f -> sendFile "/img" chatName f
|
2023-10-29 19:06:32 +00:00
|
|
|
_ -> liftRH remoteHostId $ remoteSend rhc s
|
|
|
|
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
|
|
|
|
liftRH remoteHostId $ remoteSend rhc $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f]
|
|
|
|
cryptoFileStr CryptoFile {filePath, cryptoArgs} =
|
|
|
|
maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs
|
|
|
|
<> encodeUtf8 (T.pack filePath)
|
|
|
|
processRemoteCommand _ _ _ _ = pure $ chatCmdError Nothing "remote command sent before session started"
|
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
|
|
|
|
liftRH rhId = liftError (ChatErrorRemoteHost rhId . RHProtocolError)
|
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-01 19:08:36 +00:00
|
|
|
findKnownRemoteCtrl :: forall m. ChatMonad m => (ByteString -> m ChatResponse) -> m ()
|
|
|
|
findKnownRemoteCtrl execChatCommand = do
|
2023-10-22 11:42:19 +03:00
|
|
|
logInfo "Starting remote host"
|
|
|
|
checkNoRemoteCtrlSession -- tiny race with the final @chatWriteVar@ until the setup finishes and supervisor spawned
|
|
|
|
discovered <- newTVarIO mempty
|
|
|
|
discoverer <- async $ discoverRemoteCtrls discovered -- TODO extract to a controller service singleton
|
2023-10-15 14:17:36 +01:00
|
|
|
size <- asks $ tbqSize . config
|
|
|
|
remoteOutputQ <- newTBQueueIO size
|
2023-11-01 19:08:36 +00:00
|
|
|
confirmed <- newEmptyTMVarIO
|
|
|
|
verified <- newEmptyTMVarIO
|
|
|
|
supervisor <- async $ do
|
|
|
|
threadDelay 500000 -- give chat controller a chance to reply with "ok" to prevent flaking tests
|
|
|
|
runHost discovered confirmed verified $ handleRemoteCommand execChatCommand remoteOutputQ
|
|
|
|
chatWriteVar remoteCtrlSession $ Just RemoteCtrlSession {discoverer, supervisor, hostServer = Nothing, discovered, confirmed, verified, remoteOutputQ}
|
2023-10-22 11:42:19 +03:00
|
|
|
|
|
|
|
-- | Track remote host lifecycle in controller session state and signal UI on its progress
|
2023-11-01 19:08:36 +00:00
|
|
|
runHost :: ChatMonad m => TM.TMap C.KeyHash (TransportHost, Word16) -> TMVar RemoteCtrlId -> TMVar (RemoteCtrlId, Text) -> (HTTP2Request -> m ()) -> m ()
|
|
|
|
runHost discovered confirmed verified handleHttp = do
|
|
|
|
remoteCtrlId <- atomically (readTMVar confirmed) -- wait for discoverRemoteCtrls.process or confirmRemoteCtrl to confirm fingerprint as a known RC
|
2023-10-22 11:42:19 +03:00
|
|
|
rc@RemoteCtrl {fingerprint} <- withStore (`getRemoteCtrl` remoteCtrlId)
|
2023-10-30 16:00:54 +02:00
|
|
|
serviceAddress <- atomically $ TM.lookup fingerprint discovered >>= maybe retry pure -- wait for location of the matching fingerprint
|
2023-10-22 11:42:19 +03:00
|
|
|
toView $ CRRemoteCtrlConnecting $ remoteCtrlInfo rc False
|
|
|
|
atomically $ writeTVar discovered mempty -- flush unused sources
|
2023-11-01 19:08:36 +00:00
|
|
|
server <- async $
|
|
|
|
-- spawn server for remote protocol commands
|
|
|
|
Discovery.connectTLSClient serviceAddress fingerprint $ \tls -> do
|
|
|
|
let sessionCode = decodeUtf8 . strEncode $ tlsUniq tls
|
|
|
|
toView $ CRRemoteCtrlSessionCode {remoteCtrl = remoteCtrlInfo rc True, sessionCode, newCtrl = False}
|
|
|
|
userInfo <- atomically $ readTMVar verified
|
|
|
|
if userInfo == (remoteCtrlId, sessionCode)
|
|
|
|
then do
|
|
|
|
toView $ CRRemoteCtrlConnected $ remoteCtrlInfo rc True
|
|
|
|
attachHTTP2Server handleHttp tls
|
|
|
|
else do
|
|
|
|
toView $ CRChatCmdError Nothing $ ChatErrorRemoteCtrl RCEBadVerificationCode
|
|
|
|
-- the server doesn't enter its loop and waitCatch below falls through
|
2023-10-22 11:42:19 +03:00
|
|
|
chatModifyVar remoteCtrlSession $ fmap $ \s -> s {hostServer = Just server}
|
|
|
|
_ <- waitCatch server -- wait for the server to finish
|
|
|
|
chatWriteVar remoteCtrlSession Nothing
|
|
|
|
toView CRRemoteCtrlStopped
|
|
|
|
|
2023-11-01 19:08:36 +00:00
|
|
|
handleRemoteCommand :: forall m. ChatMonad m => (ByteString -> m ChatResponse) -> TBQueue ChatResponse -> HTTP2Request -> m ()
|
2023-10-22 11:42:19 +03:00
|
|
|
handleRemoteCommand execChatCommand remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
|
|
|
|
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
|
|
|
|
(getNext,) <$> liftEitherWith (RPEInvalidJSON . T.pack) (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
|
|
|
RCHello {deviceName = desktopName} -> handleHello desktopName >>= reply
|
|
|
|
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
|
|
|
|
|
|
|
|
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 #-}
|
|
|
|
|
|
|
|
handleHello :: ChatMonad m => Text -> m RemoteResponse
|
|
|
|
handleHello desktopName = do
|
|
|
|
logInfo $ "Hello from " <> tshow desktopName
|
|
|
|
mobileName <- chatReadVar localDeviceName
|
2023-10-29 19:06:32 +00:00
|
|
|
encryptFiles <- chatReadVar encryptLocalFiles
|
|
|
|
pure RRHello {encoding = localEncoding, deviceName = mobileName, encryptFiles}
|
2023-10-22 11:42:19 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
subscribers <- asks multicastSubscribers
|
|
|
|
Discovery.withListener subscribers run
|
2023-09-29 14:56:56 +03:00
|
|
|
where
|
2023-10-30 16:00:54 +02:00
|
|
|
run sock = receive sock >>= process sock
|
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
receive sock =
|
2023-09-29 14:56:56 +03:00
|
|
|
Discovery.recvAnnounce sock >>= \case
|
2023-10-30 16:00:54 +02:00
|
|
|
(SockAddrInet _sockPort sockAddr, sigAnnBytes) -> case smpDecode sigAnnBytes of
|
|
|
|
Right (SignedAnnounce ann _sig) -> pure (sockAddr, ann)
|
|
|
|
Left _ -> receive sock -- TODO it is probably better to report errors to view here
|
2023-10-15 14:17:36 +01:00
|
|
|
_nonV4 -> receive sock
|
2023-10-30 16:00:54 +02:00
|
|
|
|
2023-11-01 19:08:36 +00:00
|
|
|
process sock (sockAddr, Announce {caFingerprint, serviceAddress = (annAddr, port)}) = do
|
2023-10-30 16:00:54 +02:00
|
|
|
unless (annAddr == sockAddr) $ logError "Announced address doesn't match socket address"
|
2023-10-15 14:17:36 +01:00
|
|
|
let addr = THIPv4 (hostAddressToTuple sockAddr)
|
|
|
|
ifM
|
2023-10-30 16:00:54 +02:00
|
|
|
(atomically $ TM.member caFingerprint discovered)
|
|
|
|
(logDebug $ "Fingerprint already known: " <> tshow (addr, caFingerprint))
|
2023-10-15 14:17:36 +01:00
|
|
|
( do
|
2023-10-30 16:00:54 +02:00
|
|
|
logInfo $ "New fingerprint announced: " <> tshow (addr, caFingerprint)
|
|
|
|
atomically $ TM.insert caFingerprint (addr, port) discovered
|
2023-10-15 14:17:36 +01:00
|
|
|
)
|
|
|
|
-- TODO we check fingerprint for duplicate where id doesn't matter - to prevent re-insert - and don't check to prevent duplicate events,
|
|
|
|
-- so UI now will have to check for duplicates again
|
2023-10-30 16:00:54 +02:00
|
|
|
withStore' (`getRemoteCtrlByFingerprint` caFingerprint) >>= \case
|
|
|
|
Nothing -> toView $ CRRemoteCtrlAnnounce caFingerprint -- unknown controller, ui "register" action required
|
2023-10-15 14:17:36 +01:00
|
|
|
-- TODO Maybe Bool is very confusing - the intent is very unclear here
|
|
|
|
Just found@RemoteCtrl {remoteCtrlId, accepted = storedChoice} -> case storedChoice of
|
|
|
|
Nothing -> toView $ CRRemoteCtrlFound $ remoteCtrlInfo found False -- first-time controller, ui "accept" action required
|
2023-10-30 16:00:54 +02:00
|
|
|
Just False -> run sock -- restart, skipping a rejected item
|
2023-10-15 14:17:36 +01:00
|
|
|
Just True ->
|
|
|
|
chatReadVar remoteCtrlSession >>= \case
|
|
|
|
Nothing -> toView . CRChatError Nothing . ChatError $ CEInternalError "Remote host found without running a session"
|
2023-11-01 19:08:36 +00:00
|
|
|
Just RemoteCtrlSession {confirmed} -> atomically $ void $ tryPutTMVar confirmed remoteCtrlId -- previously accepted controller, connect automatically
|
2023-10-15 14:17:36 +01:00
|
|
|
|
|
|
|
listRemoteCtrls :: ChatMonad m => m [RemoteCtrlInfo]
|
2023-10-04 18:36:10 +03:00
|
|
|
listRemoteCtrls = do
|
|
|
|
active <-
|
2023-11-01 19:08:36 +00:00
|
|
|
chatReadVar remoteCtrlSession $>>= \RemoteCtrlSession {confirmed} ->
|
|
|
|
atomically $ tryReadTMVar confirmed
|
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
|
|
|
|
remoteCtrlInfo RemoteCtrl {remoteCtrlId, displayName, fingerprint, accepted} sessionActive =
|
|
|
|
RemoteCtrlInfo {remoteCtrlId, displayName, fingerprint, accepted, sessionActive}
|
2023-10-04 18:36:10 +03:00
|
|
|
|
2023-11-01 19:08:36 +00:00
|
|
|
confirmRemoteCtrl :: ChatMonad m => RemoteCtrlId -> m ()
|
|
|
|
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-01 19:08:36 +00:00
|
|
|
RemoteCtrlSession {confirmed} <- getRemoteCtrlSession
|
2023-10-15 14:17:36 +01:00
|
|
|
withStore' $ \db -> markRemoteCtrlResolution db rcId True
|
2023-11-01 19:08:36 +00:00
|
|
|
atomically . void $ tryPutTMVar confirmed rcId -- the remote host can now proceed with connection
|
|
|
|
|
|
|
|
verifyRemoteCtrlSession :: ChatMonad m => RemoteCtrlId -> Text -> m ()
|
|
|
|
verifyRemoteCtrlSession rcId sessId = do
|
|
|
|
RemoteCtrlSession {verified} <- getRemoteCtrlSession
|
|
|
|
void . atomically $ tryPutTMVar verified (rcId, sessId)
|
2023-10-15 14:17:36 +01:00
|
|
|
|
|
|
|
stopRemoteCtrl :: ChatMonad m => m ()
|
|
|
|
stopRemoteCtrl = do
|
|
|
|
rcs <- getRemoteCtrlSession
|
|
|
|
cancelRemoteCtrlSession rcs $ chatWriteVar remoteCtrlSession Nothing
|
|
|
|
|
|
|
|
cancelRemoteCtrlSession_ :: MonadUnliftIO m => RemoteCtrlSession -> m ()
|
2023-10-07 16:23:24 +03:00
|
|
|
cancelRemoteCtrlSession_ rcs = cancelRemoteCtrlSession rcs $ pure ()
|
|
|
|
|
2023-10-15 14:17:36 +01:00
|
|
|
cancelRemoteCtrlSession :: MonadUnliftIO m => RemoteCtrlSession -> m () -> m ()
|
2023-10-07 16:23:24 +03:00
|
|
|
cancelRemoteCtrlSession RemoteCtrlSession {discoverer, supervisor, hostServer} cleanup = do
|
|
|
|
cancel discoverer -- may be gone by now
|
|
|
|
case hostServer of
|
|
|
|
Just host -> cancel host -- supervisor will clean up
|
|
|
|
Nothing -> do
|
|
|
|
cancel supervisor -- supervisor is blocked until session progresses
|
|
|
|
cleanup
|
|
|
|
|
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
|
|
|
|
|
|
|
utf8String :: [Char] -> ByteString
|
|
|
|
utf8String = encodeUtf8 . T.pack
|
|
|
|
{-# INLINE utf8String #-}
|