mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-04-01 03:05:57 +00:00
114 lines
4.0 KiB
Haskell
114 lines
4.0 KiB
Haskell
{-# LANGUAGE DuplicateRecordFields #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE MultiWayIf #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
module Simplex.FileTransfer.Transport
|
|
( supportedFileServerVRange,
|
|
XFTPRcvChunkSpec (..),
|
|
sendFile,
|
|
receiveFile,
|
|
sendEncFile,
|
|
receiveEncFile,
|
|
)
|
|
where
|
|
|
|
import qualified Control.Exception as E
|
|
import Control.Monad.Except
|
|
import qualified Data.ByteArray as BA
|
|
import Data.ByteString.Builder (Builder, byteString)
|
|
import Data.ByteString.Char8 (ByteString)
|
|
import qualified Data.ByteString.Char8 as B
|
|
import qualified Data.ByteString.Lazy.Char8 as LB
|
|
import Data.Word (Word32)
|
|
import GHC.IO.Handle.Internals (ioe_EOF)
|
|
import Simplex.FileTransfer.Protocol (XFTPErrorType (..))
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
|
import Simplex.Messaging.Version
|
|
import System.IO (Handle, IOMode (..), withFile)
|
|
|
|
data XFTPRcvChunkSpec = XFTPRcvChunkSpec
|
|
{ filePath :: FilePath,
|
|
chunkSize :: Word32,
|
|
chunkDigest :: ByteString
|
|
}
|
|
deriving (Show)
|
|
|
|
supportedFileServerVRange :: VersionRange
|
|
supportedFileServerVRange = mkVersionRange 1 1
|
|
|
|
fileBlockSize :: Int
|
|
fileBlockSize = 16 * 1024
|
|
|
|
sendFile :: Handle -> (Builder -> IO ()) -> Word32 -> IO ()
|
|
sendFile h send = go
|
|
where
|
|
go 0 = pure ()
|
|
go sz =
|
|
getFileChunk h sz >>= \ch -> do
|
|
send $ byteString ch
|
|
go $ sz - fromIntegral (B.length ch)
|
|
|
|
sendEncFile :: Handle -> (Builder -> IO ()) -> LC.SbState -> Word32 -> IO ()
|
|
sendEncFile h send = go
|
|
where
|
|
go sbState 0 = do
|
|
let authTag = BA.convert (LC.sbAuth sbState)
|
|
send $ byteString authTag
|
|
go sbState sz =
|
|
getFileChunk h sz >>= \ch -> do
|
|
let (encCh, sbState') = LC.sbEncryptChunk sbState ch
|
|
send (byteString encCh) `E.catch` \(e :: E.SomeException) -> print e >> E.throwIO e
|
|
go sbState' $ sz - fromIntegral (B.length ch)
|
|
|
|
getFileChunk :: Handle -> Word32 -> IO ByteString
|
|
getFileChunk h sz =
|
|
B.hGet h fileBlockSize >>= \case
|
|
"" -> ioe_EOF
|
|
ch -> pure $ B.take (fromIntegral sz) ch -- sz >= xftpBlockSize
|
|
|
|
receiveFile :: (Int -> IO ByteString) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
|
receiveFile getBody = receiveFile_ receive
|
|
where
|
|
receive h sz = do
|
|
ch <- getBody fileBlockSize
|
|
let chSize = fromIntegral $ B.length ch
|
|
if
|
|
| chSize > sz -> pure $ Left SIZE
|
|
| chSize > 0 -> B.hPut h ch >> receive h (sz - chSize)
|
|
| sz == 0 -> pure $ Right ()
|
|
| otherwise -> pure $ Left SIZE
|
|
|
|
receiveEncFile :: (Int -> IO ByteString) -> LC.SbState -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
|
receiveEncFile getBody = receiveFile_ . receive
|
|
where
|
|
receive sbState h sz = do
|
|
ch <- getBody fileBlockSize
|
|
let chSize = fromIntegral $ B.length ch
|
|
if
|
|
| chSize > sz + authSz -> pure $ Left SIZE
|
|
| chSize > 0 -> do
|
|
let (ch', rest) = B.splitAt (fromIntegral sz) ch
|
|
(decCh, sbState') = LC.sbDecryptChunk sbState ch'
|
|
sz' = sz - fromIntegral (B.length ch')
|
|
B.hPut h decCh
|
|
if sz' > 0
|
|
then receive sbState' h sz'
|
|
else do
|
|
let tag' = B.take C.authTagSize rest
|
|
tagSz = B.length tag'
|
|
tag = LC.sbAuth sbState'
|
|
tag'' <- if tagSz == C.authTagSize then pure tag' else (tag' <>) <$> getBody (C.authTagSize - tagSz)
|
|
pure $ if BA.constEq tag'' tag then Right () else Left CRYPTO
|
|
| otherwise -> pure $ Left SIZE
|
|
authSz = fromIntegral C.authTagSize
|
|
|
|
receiveFile_ :: (Handle -> Word32 -> IO (Either XFTPErrorType ())) -> XFTPRcvChunkSpec -> ExceptT XFTPErrorType IO ()
|
|
receiveFile_ receive XFTPRcvChunkSpec {filePath, chunkSize, chunkDigest} = do
|
|
ExceptT $ withFile filePath WriteMode (`receive` chunkSize)
|
|
digest' <- liftIO $ LC.sha256Hash <$> LB.readFile filePath
|
|
when (digest' /= chunkDigest) $ throwError DIGEST
|