mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 08:05:23 +00:00
Merge branch 'master' into badges
This commit is contained in:
@@ -70,6 +70,7 @@ import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Types.UITheme
|
||||
import Simplex.Chat.Util (liftIOEither)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI)
|
||||
import Simplex.Messaging.Server.Information (ServerPublicInfo)
|
||||
import Simplex.Messaging.Agent (AgentClient, DatabaseDiff, SubscriptionsInfo)
|
||||
import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg, Worker)
|
||||
@@ -694,6 +695,11 @@ planResolveModeP =
|
||||
"never" -> pure PRMNever
|
||||
_ -> fail "bad PlanResolveMode"
|
||||
|
||||
data CommandSource
|
||||
= CSLocal -- entered on this device
|
||||
| CSRemoteHost RemoteHostId -- forwarded to a paired remote host
|
||||
| CSRemoteCtrl -- received from a paired remote controller
|
||||
|
||||
allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal
|
||||
allowRemoteCommand = \case
|
||||
StartChat {} -> False
|
||||
@@ -781,7 +787,7 @@ data ChatResponse
|
||||
| CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]}
|
||||
| CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo}
|
||||
| CRChatItemId User (Maybe ChatItemId)
|
||||
| CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure}
|
||||
| CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure, serverInfo :: Maybe (Either String ServerPublicInfo)}
|
||||
| CRChatRelayTestResult {user :: User, relayProfile :: Maybe RelayProfile, relayTestFailure :: Maybe RelayTestFailure}
|
||||
| CRServerOperatorConditions {conditions :: ServerOperatorConditions}
|
||||
| CRUserServers {user :: User, userServers :: [UserOperatorServers]}
|
||||
|
||||
@@ -97,7 +97,7 @@ runSimplexChat ChatConfig {testView} ChatOpts {coreOptions = CoreChatOpts {chatR
|
||||
waitEither_ a1 a2
|
||||
|
||||
sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse)
|
||||
sendChatCmdStr cc s = runReaderT (execChatCommand Nothing (encodeUtf8 $ T.pack s) 0) cc
|
||||
sendChatCmdStr cc s = runReaderT (execChatCommand CSLocal (encodeUtf8 $ T.pack s) 0) cc
|
||||
|
||||
sendChatCmd :: ChatController -> ChatCommand -> IO (Either ChatError ChatResponse)
|
||||
sendChatCmd cc cmd = runReaderT (execChatCommand' cmd 0) cc
|
||||
|
||||
@@ -5,14 +5,20 @@ module Simplex.Chat.Files where
|
||||
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Util (ifM)
|
||||
import System.FilePath (combine, splitExtensions)
|
||||
import System.FilePath (combine, makeValid, splitExtensions, takeFileName)
|
||||
import UnliftIO.Directory (doesDirectoryExist, doesFileExist, getHomeDirectory, getTemporaryDirectory)
|
||||
|
||||
safeFileNameStr :: String -> String
|
||||
safeFileNameStr = notDots . makeValid . takeFileName
|
||||
where
|
||||
notDots n = if n == "." || n == ".." then "_" else n
|
||||
|
||||
-- | The file name is sanitized, so the combined path cannot escape the folder.
|
||||
uniqueCombine :: FilePath -> String -> IO FilePath
|
||||
uniqueCombine fPath fName = tryCombine (0 :: Int)
|
||||
where
|
||||
tryCombine n =
|
||||
let (name, ext) = splitExtensions fName
|
||||
let (name, ext) = splitExtensions $ safeFileNameStr fName
|
||||
suffix = if n == 0 then "" else "_" <> show n
|
||||
f = fPath `combine` (name <> suffix <> ext)
|
||||
in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f)
|
||||
|
||||
@@ -378,19 +378,24 @@ useServers as opDomains uss =
|
||||
xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss
|
||||
in (smp', xftp')
|
||||
|
||||
execChatCommand :: Maybe RemoteHostId -> ByteString -> Int -> CM' (Either ChatError ChatResponse)
|
||||
execChatCommand rh s retryNum =
|
||||
execChatCommand :: CommandSource -> ByteString -> Int -> CM' (Either ChatError ChatResponse)
|
||||
execChatCommand src s retryNum =
|
||||
case parseChatCommand s of
|
||||
Left e -> pure $ chatCmdError e
|
||||
Right cmd -> case rh of
|
||||
Just rhId
|
||||
Right cmd -> case src of
|
||||
CSRemoteHost rhId
|
||||
| allowRemoteCommand cmd -> execRemoteCommand rhId cmd s retryNum
|
||||
| otherwise -> pure $ Left $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand
|
||||
_ -> do
|
||||
cc@ChatController {config = ChatConfig {chatHooks}} <- ask
|
||||
case preCmdHook chatHooks of
|
||||
Just hook -> liftIO (hook cc cmd) >>= either pure (`execChatCommand'` retryNum)
|
||||
Nothing -> execChatCommand' cmd retryNum
|
||||
CSRemoteCtrl
|
||||
| allowRemoteCommand cmd -> execLocal cmd
|
||||
| otherwise -> pure $ Left $ ChatErrorRemoteCtrl $ RCEProtocolError $ RPEInvalidBody "prohibited command"
|
||||
CSLocal -> execLocal cmd
|
||||
where
|
||||
execLocal cmd = do
|
||||
cc@ChatController {config = ChatConfig {chatHooks}} <- ask
|
||||
case preCmdHook chatHooks of
|
||||
Just hook -> liftIO (hook cc cmd) >>= either pure (`execChatCommand'` retryNum)
|
||||
Nothing -> execChatCommand' cmd retryNum
|
||||
|
||||
execChatCommand' :: ChatCommand -> Int -> CM' (Either ChatError ChatResponse)
|
||||
execChatCommand' cmd retryNum = handleCommandError $ do
|
||||
@@ -1675,8 +1680,9 @@ processChatCommand cxt nm = \case
|
||||
aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of
|
||||
Just Refl -> pure $ AUS SDBNew $ newUserServer srv
|
||||
Nothing -> throwCmdError $ "incorrect server protocol: " <> B.unpack (strEncode srv)
|
||||
APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user ->
|
||||
lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a nm (aUserId user) server)
|
||||
APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> do
|
||||
r <- lift $ withAgent' $ \a -> testProtocolServer a nm (aUserId user) server
|
||||
pure $ uncurry (CRServerTestResult user srv) $ either ((,Nothing) . Just) (Nothing,) r
|
||||
TestProtoServer srv -> withUser $ \User {userId} ->
|
||||
processChatCommand cxt nm $ APITestProtoServer userId srv
|
||||
APITestChatRelay userId address -> withUserId userId $ \user -> do
|
||||
@@ -3602,7 +3608,7 @@ processChatCommand cxt nm = \case
|
||||
ConfirmRemoteCtrl rcId -> withUser_ $ do
|
||||
(rc, ctrlAppInfo) <- confirmRemoteCtrl rcId
|
||||
pure CRRemoteCtrlConnecting {remoteCtrl_ = Just rc, ctrlAppInfo, appVersion = currentAppVersion}
|
||||
VerifyRemoteCtrlSession sessId -> withUser_ $ verifyRemoteCtrlSession (execChatCommand Nothing) sessId
|
||||
VerifyRemoteCtrlSession sessId -> withUser_ $ verifyRemoteCtrlSession (execChatCommand CSRemoteCtrl) sessId
|
||||
StopRemoteCtrl -> withUser_ $ stopRemoteCtrl >> ok_
|
||||
ListRemoteCtrls -> withUser_ $ CRRemoteCtrlList <$> listRemoteCtrls
|
||||
DeleteRemoteCtrl rc -> withUser_ $ deleteRemoteCtrl rc >> ok_
|
||||
@@ -4054,11 +4060,12 @@ processChatCommand cxt nm = \case
|
||||
lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct'
|
||||
pure $ CRContactPrefsUpdated user ct ct'
|
||||
runUpdateGroupProfile :: User -> GroupInfo -> GroupProfile -> Bool -> CM ChatResponse
|
||||
runUpdateGroupProfile user gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} p'@GroupProfile {displayName = n', image = img'} domainVerified = do
|
||||
runUpdateGroupProfile user gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} p'@GroupProfile {displayName = n', image = img', memberAdmission = ma'} domainVerified = do
|
||||
assertUserGroupRole gInfo GROwner
|
||||
when (n /= n') $ checkValidName n'
|
||||
checkProfileImageSize img'
|
||||
checkGroupProfileSize p'
|
||||
when (useRelays' gInfo && isJust (ma' >>= review)) $ throwCmdError "Admission review is not supported in channels"
|
||||
-- updateGroupProfile clears domain verification; re-set it when the caller already re-resolved the name
|
||||
gInfo' <- withStore $ \db -> do
|
||||
g <- updateGroupProfile db user gInfo p'
|
||||
@@ -4210,10 +4217,11 @@ processChatCommand cxt nm = \case
|
||||
groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName
|
||||
pure (groupId, groupMemberId)
|
||||
newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> CM GroupInfo
|
||||
newGroup user incognito gProfile@GroupProfile {displayName, image} useRelays memberId groupKeys_ publicMemberCount_ = do
|
||||
newGroup user incognito gProfile@GroupProfile {displayName, image, memberAdmission} useRelays memberId groupKeys_ publicMemberCount_ = do
|
||||
checkValidName displayName
|
||||
checkProfileImageSize image
|
||||
checkGroupProfileSize gProfile
|
||||
when (useRelays && isJust (memberAdmission >>= review)) $ throwCmdError "Admission review is not supported in channels"
|
||||
-- [incognito] generate incognito profile for group membership
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile useRelays memberId groupKeys_ publicMemberCount_
|
||||
|
||||
@@ -48,7 +48,7 @@ import Data.Word (Word32)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Delivery
|
||||
import Simplex.Chat.Files (getChatTempDirectory)
|
||||
import Simplex.Chat.Files (getChatTempDirectory, safeFileNameStr)
|
||||
import Simplex.Chat.Library.Internal
|
||||
import Simplex.Chat.Web (channelContentChanged, channelProfileUpdated, channelRemoved)
|
||||
import Simplex.Chat.Messages
|
||||
@@ -101,7 +101,6 @@ import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (TransportError (..))
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import qualified System.FilePath as FP
|
||||
import System.Mem.Weak (Weak)
|
||||
import Text.Read (readMaybe)
|
||||
import UnliftIO.Concurrent (ThreadId, forkIO, mkWeakThreadId)
|
||||
@@ -916,7 +915,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
if useRelays' gInfo''
|
||||
then do
|
||||
introduceInChannel cxt user gInfo'' m'
|
||||
when (groupFeatureAllowed SGFHistory gInfo'') $ sendHistory user gInfo'' m'
|
||||
case mStatus of
|
||||
GSMemPendingApproval -> pure ()
|
||||
GSMemPendingReview -> pure ()
|
||||
_ -> when (groupFeatureAllowed SGFHistory gInfo'') $ sendHistory user gInfo'' m'
|
||||
else case mStatus of
|
||||
GSMemPendingApproval -> pure ()
|
||||
GSMemPendingReview -> introduceToModerators cxt user gInfo'' m'
|
||||
@@ -1963,7 +1965,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol})
|
||||
|
||||
mkValidFileInvitation :: FileInvitation -> FileInvitation
|
||||
mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = FP.makeValid $ FP.takeFileName fileName}
|
||||
mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = safeFileNameStr fileName}
|
||||
|
||||
validateFileInvitation :: FileInvitation -> CM FileInvitation
|
||||
validateFileInvitation fInv@FileInvitation {fileName, fileSize}
|
||||
@@ -3885,7 +3887,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
Just author -> action author
|
||||
Nothing -> messageError $ "x.grp.msg.forward: event " <> tshow tag <> " requires author"
|
||||
|
||||
withVerifiedMsg :: MsgEncodingI e => GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> ParsedMsg e -> UTCTime -> (VerifiedMsg e -> CM a) -> CM (Maybe a)
|
||||
withVerifiedMsg :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> ParsedMsg e -> UTCTime -> (VerifiedMsg e -> CM a) -> CM (Maybe a)
|
||||
withVerifiedMsg gInfo@GroupInfo {membership} scopeInfo member (ParsedMsg _ signedMsg_ chatMsg@ChatMessage {chatMsgEvent}) ts action =
|
||||
case verified of
|
||||
Just verifiedMsg -> Just <$> action verifiedMsg
|
||||
|
||||
@@ -63,7 +63,7 @@ batchMessages mode maxLen = addBatch . foldr addToBatch ([], [], [], 0, 0)
|
||||
addToBatch :: Either ChatError SndMessage -> ([Either ChatError MsgBatch], [ByteString], [SndMessage], Int, Int) -> ([Either ChatError MsgBatch], [ByteString], [SndMessage], Int, Int)
|
||||
addToBatch (Left err) acc = (Left err : addBatch acc, [], [], 0, 0) -- step over original error
|
||||
addToBatch (Right msg@SndMessage {msgBody, signedMsg_}) acc@(batches, bodies, msgs, len, n)
|
||||
| batchLen mode len' n' <= maxLen = (batches, body : bodies, msg : msgs, len', n')
|
||||
| n' <= maxBatchElementCount && batchLen mode len' n' <= maxLen = (batches, body : bodies, msg : msgs, len', n')
|
||||
| msgLen <= maxLen = (addBatch acc, [body], [msg], msgLen, 1)
|
||||
| otherwise = (errLarge msg : addBatch acc, [], [], 0, 0)
|
||||
where
|
||||
@@ -90,7 +90,7 @@ batchDeliveryTasks1 _vr maxLen = toResult . foldl' addToBatch ([], [], [], 0, 0)
|
||||
| msgLen + 4 > maxLen = (msgBodies, accepted, task : large, len, n)
|
||||
-- fits: include in batch
|
||||
-- batch overhead: '=' + count (2) + 2-byte length prefix per element
|
||||
| len' + (n + 1) * 2 + 2 <= maxLen = (msgBody : msgBodies, task : accepted, large, len', n + 1)
|
||||
| n + 1 <= maxBatchElementCount && len' + (n + 1) * 2 + 2 <= maxLen = (msgBody : msgBodies, task : accepted, large, len', n + 1)
|
||||
-- doesn't fit: stop adding further messages
|
||||
| otherwise = (msgBodies, accepted, large, len, n)
|
||||
where
|
||||
@@ -112,7 +112,7 @@ batchElements maxLen = finish . foldl' addToBatch ([], [], 0, 0, 0)
|
||||
where
|
||||
addToBatch (batches, elems, len, n, dropped) el
|
||||
| elLen + 4 > maxLen = (batches, elems, len, n, dropped + 1)
|
||||
| len + elLen + (n + 1) * 2 + 2 <= maxLen = (batches, el : elems, len + elLen, n + 1, dropped)
|
||||
| n + 1 <= maxBatchElementCount && len + elLen + (n + 1) * 2 + 2 <= maxLen = (batches, el : elems, len + elLen, n + 1, dropped)
|
||||
| otherwise = (closeBatch elems : batches, [el], elLen, 1, dropped)
|
||||
where
|
||||
elLen = B.length el
|
||||
@@ -182,7 +182,7 @@ batchProfilesWithBody maxLen body labeled =
|
||||
initState = (initLen, initCount, [], [], [])
|
||||
step (totalLen, count, acceptedPairs, overflow, large) (s, e)
|
||||
| B.length e + 4 > maxLen = (totalLen, count, acceptedPairs, overflow, s : large)
|
||||
| count >= 255 = full
|
||||
| count >= maxBatchElementCount = full
|
||||
| candidateLen <= maxLen = (candidateLen, count + 1, (s, e) : acceptedPairs, overflow, large)
|
||||
| otherwise = full
|
||||
where
|
||||
@@ -215,7 +215,7 @@ batchProfiles maxLen =
|
||||
addToBatch (s, e) acc@(batches, elems, members, len, n, large)
|
||||
| B.length e + 4 > maxLen = (batches, elems, members, len, n, s : large)
|
||||
-- batch overhead: '=' + count (2) + 2-byte length prefix per element
|
||||
| n + 1 <= 255 && len + B.length e + (n + 1) * 2 + 2 <= maxLen =
|
||||
| n + 1 <= maxBatchElementCount && len + B.length e + (n + 1) * 2 + 2 <= maxLen =
|
||||
(batches, e : elems, s : members, len + B.length e, n + 1, large)
|
||||
-- doesn't fit current — flush and start new with this element alone
|
||||
| otherwise =
|
||||
|
||||
@@ -352,7 +352,7 @@ chatSendCmd cc cmd = chatSendRemoteCmdRetry cc Nothing cmd 0
|
||||
{-# INLINE chatSendCmd #-}
|
||||
|
||||
chatSendRemoteCmdRetry :: ChatController -> Maybe RemoteHostId -> B.ByteString -> Int -> IO JSONByteString
|
||||
chatSendRemoteCmdRetry cc rh s retryNum = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s retryNum) cc
|
||||
chatSendRemoteCmdRetry cc rh s retryNum = J.encode . eitherToResult rh <$> runReaderT (execChatCommand (maybe CSLocal CSRemoteHost rh) s retryNum) cc
|
||||
|
||||
chatRecvMsg :: ChatController -> IO JSONByteString
|
||||
chatRecvMsg ChatController {outputQ} = J.encode . uncurry eitherToResult <$> readChatResponse
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DerivingStrategies #-}
|
||||
@@ -909,6 +910,10 @@ maxCompressedMsgLength = 13380
|
||||
maxDecompressedMsgLength :: Int
|
||||
maxDecompressedMsgLength = 65536
|
||||
|
||||
-- Applies to all batch formats; 255 is the maximum for the 1-byte count in the binary batch format.
|
||||
maxBatchElementCount :: Int
|
||||
maxBatchElementCount = 255
|
||||
|
||||
-- Defensive entry-count bound for the roster blob parser (rosterBlobP) and the
|
||||
-- promotion cap over the promoted (member/moderator/admin) set.
|
||||
maxGroupRosterSize :: Int
|
||||
@@ -953,10 +958,16 @@ encodeChatMessage maxSize msg = do
|
||||
|
||||
parseChatMessages :: ByteString -> [Either String AParsedMsg]
|
||||
parseChatMessages "" = [Left "empty string"]
|
||||
parseChatMessages msg = case B.head msg of
|
||||
parseChatMessages msg = checkBatchLimit $ case B.head msg of
|
||||
'X' -> decodeCompressed (B.tail msg)
|
||||
c -> parseUncompressed c msg
|
||||
where
|
||||
checkBatchLimit ms
|
||||
| ms `lengthLE` maxBatchElementCount = ms
|
||||
| otherwise = [Left "too many messages in batch"]
|
||||
lengthLE :: [a] -> Int -> Bool
|
||||
[] `lengthLE` !n = n >= 0
|
||||
(_ : xs) `lengthLE` !n = n > 0 && xs `lengthLE` (n - 1)
|
||||
parseUncompressed c s = case c of
|
||||
'[' -> case J.eitherDecodeStrict' s of
|
||||
Right v -> map (fmap plainMsg . parseItem) v
|
||||
|
||||
@@ -65,10 +65,10 @@ import Simplex.Messaging.Util
|
||||
import Simplex.RemoteControl.Client
|
||||
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..), RCVerifiedInvitation (..), verifySignedInvitation)
|
||||
import Simplex.RemoteControl.Types
|
||||
import System.FilePath (takeFileName, (</>))
|
||||
import System.FilePath (takeDirectory, takeFileName, (</>))
|
||||
import UnliftIO
|
||||
import UnliftIO.Concurrent (forkIO)
|
||||
import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, removeDirectoryRecursive, renameFile)
|
||||
import UnliftIO.Directory (canonicalizePath, copyFile, createDirectoryIfMissing, doesDirectoryExist, removeDirectoryRecursive, renameFile)
|
||||
|
||||
remoteFilesFolder :: String
|
||||
remoteFilesFolder = "simplex_v1_files"
|
||||
@@ -553,7 +553,7 @@ liftRC = liftError (ChatErrorRemoteCtrl . RCEProtocolError)
|
||||
handleSend :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> Int -> CM' RemoteResponse
|
||||
handleSend execCC command retryNum = do
|
||||
logDebug $ "Send: " <> tshow command
|
||||
-- execCC checks for remote-allowed commands
|
||||
-- execCC is execChatCommand CSRemoteCtrl, which checks allowRemoteCommand
|
||||
-- convert errors thrown in execCC into error responses to prevent aborting the protocol wrapper
|
||||
RRChatResponse . eitherToResult <$> execCC (encodeUtf8 command) retryNum
|
||||
|
||||
@@ -574,10 +574,19 @@ handleStoreFile rfKN fileName fileSize fileDigest getChunk =
|
||||
Nothing -> storeFileTo =<< getDefaultFilesFolder
|
||||
storeFileTo :: FilePath -> CM' (Either RemoteProtocolError FilePath)
|
||||
storeFileTo dir = liftIO . tryAllErrors' $ do
|
||||
unless (validRemoteFileName fileName) $ throwError $ RPEInvalidBody "invalid file name"
|
||||
filePath <- liftIO $ dir `uniqueCombine` fileName
|
||||
-- resolves symlinks, so it also catches a final component linking outside the folder
|
||||
canonPath <- liftIO $ canonicalizePath filePath
|
||||
inDir <- liftIO $ (takeDirectory canonPath ==) <$> canonicalizePath dir
|
||||
unless inDir $ throwError $ RPEInvalidBody "file path outside of files folder"
|
||||
receiveEncryptedFile rfKN getChunk fileSize fileDigest filePath
|
||||
pure filePath
|
||||
|
||||
-- The controller only ever sends a bare file name (see storeRemoteFile), so a path is a protocol violation.
|
||||
validRemoteFileName :: FilePath -> Bool
|
||||
validRemoteFileName fName = fName == takeFileName fName && fName `notElem` (["", ".", ".."] :: [FilePath])
|
||||
|
||||
handleGetFile :: User -> RemoteFile -> Respond -> CM ()
|
||||
handleGetFile User {userId} RemoteFile {userId = commandUserId, fileId, sent, fileSource = cf'@CryptoFile {filePath}} reply = do
|
||||
logDebug $ "GetFile: " <> tshow filePath
|
||||
|
||||
@@ -62,7 +62,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
|
||||
cmd = parseChatCommand bs
|
||||
rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing
|
||||
unless (isMessage cmd) $ echo s
|
||||
r <- execChatCommand rh' bs 0 `runReaderT` cc
|
||||
r <- execChatCommand (maybe CSLocal CSRemoteHost rh') bs 0 `runReaderT` cc
|
||||
case r of
|
||||
Right r' -> processResp cmd rh r'
|
||||
Left _ -> when (isMessage cmd) $ echo s
|
||||
|
||||
@@ -167,7 +167,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha
|
||||
_ -> pure ()
|
||||
logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s
|
||||
getRemoteUser rhId =
|
||||
runReaderT (execChatCommand (Just rhId) "/user" 0) cc >>= \case
|
||||
runReaderT (execChatCommand (CSRemoteHost rhId) "/user" 0) cc >>= \case
|
||||
Right CRActiveUser {user} -> updateRemoteUser ct user rhId
|
||||
cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr
|
||||
removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct)
|
||||
|
||||
@@ -126,7 +126,11 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
|
||||
CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat]
|
||||
CRChatContentTypes cts -> [plain $ "Chat content types: " <> T.intercalate ", " (map (safeDecodeUtf8 . strEncode) cts)]
|
||||
CRChatTags u tags -> ttyUser u [viewJSON tags]
|
||||
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
|
||||
CRServerTestResult u srv testFailure info -> ttyUser u $ viewServerTestResult srv testFailure <> maybe [] viewServerInfo info
|
||||
where
|
||||
viewServerInfo = \case
|
||||
Left e -> [plain $ "Server Info Error: " <> T.pack e]
|
||||
Right i -> [plain $ "Server Info: " <> tshow i]
|
||||
CRChatRelayTestResult u relayProfile_ relayTestFailure_ -> ttyUser u $ viewRelayTestResult relayProfile_ relayTestFailure_
|
||||
CRServerOperatorConditions (ServerOperatorConditions ops _ ca) -> viewServerOperators ops ca
|
||||
CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp)
|
||||
|
||||
Reference in New Issue
Block a user