Skip to content

Commit 79d1b48

Browse files
ed-asriyanshumvgoloveepoberezkin
authored
feat: add server public information handling in XFTP protocol (#1846)
* feat: add server public information handling in XFTP protocol * test --------- Co-authored-by: sh <github.shum@liber.li> Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
1 parent ac4bf69 commit 79d1b48

11 files changed

Lines changed: 47 additions & 22 deletions

File tree

protocol/xftp.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Version 3, 2025-01-24
1+
Version 4, 2026-08-08
22

33
# SimpleX File Transfer Protocol
44

@@ -50,11 +50,12 @@ The objective of SimpleX File Transfer Protocol (XFTP) is to facilitate the secu
5050

5151
XFTP is implemented as an application level protocol on top of HTTP2 and TLS.
5252

53-
This document describes XFTP protocol version 3. The version history:
53+
This document describes XFTP protocol version 4. The version history:
5454

5555
- v1: initial version
5656
- v2: authenticated commands - added basic auth support for commands
5757
- v3: blocked files - added BLOCKED error type for policy violations
58+
- v4: server public information in handshake
5859

5960
The protocol describes the set of commands that senders and recipients can send to XFTP routers to create, upload, download and delete data packets of several pre-defined sizes. XFTP routers SHOULD support packets of 4 sizes: 64KB, 256KB, 1MB and 4MB (1KB = 1024 bytes, 1MB = 1024KB).
6061

src/Simplex/FileTransfer/Client.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import Control.Monad
3838
import Control.Monad.Except
3939
import Control.Monad.Trans.Except
4040
import Crypto.Random (ChaChaDRG)
41+
import qualified Data.Aeson as J
4142
import Data.Bifunctor (first)
4243
import Data.ByteString.Builder (Builder, byteString)
4344
import Data.ByteString.Char8 (ByteString)
@@ -155,12 +156,13 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN,
155156

156157
xftpClientHandshakeV1 :: VersionRangeXFTP -> C.KeyHash -> HTTP2Client -> THandleParamsXFTP 'TClient -> ExceptT XFTPClientError IO (THandleParamsXFTP 'TClient)
157158
xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {sessionId, serverKey} thParams0 = do
158-
shs@XFTPServerHandshake {authPubKey = ck} <- getServerHandshake
159+
shs@XFTPServerHandshake {authPubKey = ck, serverInfoBytes} <- getServerHandshake
159160
(vr, sk) <- processServerHandshake shs
160161
let v = maxVersion vr
161162
sendClientHandshake XFTPClientHandshake {xftpVersion = v, keyHash}
162163
let thAuth = Just THAuthClient {peerServerPubKey = sk, peerServerCertKey = ck, clientService = Nothing, sessSecret = Nothing}
163-
pure thParams0 {thAuth, thVersion = v, thServerVRange = vr}
164+
serverInfo = J.eitherDecodeStrict' <$> serverInfoBytes
165+
pure thParams0 {thAuth, thVersion = v, thServerVRange = vr, serverInfo}
164166
where
165167
getServerHandshake :: ExceptT XFTPClientError IO XFTPServerHandshake
166168
getServerHandshake = do

src/Simplex/FileTransfer/Server.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import Control.Monad
2323
import Control.Monad.Except
2424
import Control.Monad.Reader
2525
import Control.Monad.Trans.Except
26+
import qualified Data.Aeson as J
2627
import Data.Bifunctor (first)
2728
import qualified Data.ByteString.Base64.URL as B64
2829
import Data.ByteString.Builder (Builder, byteString)
2930
import Data.ByteString.Char8 (ByteString)
3031
import qualified Data.ByteString.Char8 as B
32+
import qualified Data.ByteString.Lazy.Char8 as LB
3133
import Data.Int (Int64)
3234
import Data.List.NonEmpty (NonEmpty)
3335
import qualified Data.List.NonEmpty as L
@@ -124,7 +126,7 @@ data Handshake
124126
| HandshakeAccepted (THandleParams XFTPVersion 'TServer)
125127

126128
xftpServer :: forall s. FileStoreClass s => XFTPServerConfig s -> TMVar Bool -> M s ()
127-
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange} started = do
129+
xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpiration, fileExpiration, xftpServerVRange, information} started = do
128130
mapM_ (expireServerFiles Nothing) fileExpiration
129131
restoreServerStats
130132
raceAny_
@@ -202,7 +204,8 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
202204
fst kp <$ TM.insert sessionId (HandshakeSent $ snd kp) sessions
203205
let authPubKey = CertChainPubKey chain (C.signX509 serverSignKey $ C.publicToX509 k)
204206
webIdentityProof = C.sign serverSignKey . (<> sessionId) <$> challenge_
205-
let hs = XFTPServerHandshake {xftpVersionRange = xftpServerVRange, sessionId, authPubKey, webIdentityProof}
207+
serverInfoBytes = LB.toStrict . J.encode <$> information
208+
let hs = XFTPServerHandshake {xftpVersionRange = xftpServerVRange, sessionId, authPubKey, webIdentityProof, serverInfoBytes}
206209
shs <- encodeXftp hs
207210
#ifdef slow_servers
208211
lift randomDelay

src/Simplex/FileTransfer/Server/Env.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import Simplex.FileTransfer.Transport (VersionRangeXFTP)
6464
import qualified Simplex.Messaging.Crypto as C
6565
import Simplex.Messaging.Protocol (BasicAuth, RcvPublicAuthKey)
6666
import Simplex.Messaging.Server.Expiration
67+
import Simplex.Messaging.Server.Information (ServerPublicInfo)
6768
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), loadFingerprint, loadServerCredential)
6869
import Simplex.Messaging.Util (tshow)
6970
import System.IO (IOMode (..))
@@ -97,6 +98,8 @@ data XFTPServerConfig s = XFTPServerConfig
9798
httpCredentials :: Maybe ServerCredentials,
9899
-- | XFTP client-server protocol version range
99100
xftpServerVRange :: VersionRangeXFTP,
101+
-- | server public information sent in handshake and used to generate static mini-site
102+
information :: Maybe ServerPublicInfo,
100103
-- stats config - see SMP server config
101104
logStatsInterval :: Maybe Int64,
102105
logStatsStartTime :: Int64,

src/Simplex/FileTransfer/Server/Main.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ xftpServerCLI_ generateSite serveStaticFiles cfgPath logPath = do
306306
},
307307
httpCredentials = httpCredentials_,
308308
xftpServerVRange = supportedFileServerVRange,
309+
information = serverPublicInfo ini,
309310
logStatsInterval = logStats $> 86400, -- seconds
310311
logStatsStartTime = 0, -- seconds from 00:00 UTC
311312
serverStatsLogFile = combine logPath "file-server-stats.daily.log",

src/Simplex/FileTransfer/Transport.hs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Simplex.FileTransfer.Transport
1212
( supportedFileServerVRange,
1313
authCmdsXFTPVersion,
1414
blockedFilesXFTPVersion,
15+
serverInfoXFTPVersion,
1516
xftpClientHandshakeStub,
1617
alpnSupportedXFTPhandshakes,
1718
xftpALPNv1,
@@ -36,7 +37,6 @@ module Simplex.FileTransfer.Transport
3637
)
3738
where
3839

39-
import Control.Applicative (optional)
4040
import qualified Control.Exception as E
4141
import Control.Logger.Simple
4242
import Control.Monad
@@ -62,7 +62,7 @@ import Simplex.Messaging.Parsers
6262
import Simplex.Messaging.Protocol (BlockingInfo, CommandError)
6363
import Simplex.Messaging.Transport (ALPN, CertChainPubKey, ServiceCredentials, SessionId, THandle (..), THandleParams (..), TransportError (..), TransportPeer (..))
6464
import Simplex.Messaging.Transport.HTTP2.File
65-
import Simplex.Messaging.Util (bshow, tshow, (<$?>))
65+
import Simplex.Messaging.Util (bshow, tshow, (<$?>), (<$$>))
6666
import Simplex.Messaging.Version
6767
import Simplex.Messaging.Version.Internal
6868
import System.IO (Handle, IOMode (..), withFile)
@@ -97,8 +97,11 @@ authCmdsXFTPVersion = VersionXFTP 2
9797
blockedFilesXFTPVersion :: VersionXFTP
9898
blockedFilesXFTPVersion = VersionXFTP 3
9999

100+
serverInfoXFTPVersion :: VersionXFTP
101+
serverInfoXFTPVersion = VersionXFTP 4
102+
100103
currentXFTPVersion :: VersionXFTP
101-
currentXFTPVersion = VersionXFTP 3
104+
currentXFTPVersion = VersionXFTP 4
102105

103106
supportedFileServerVRange :: VersionRangeXFTP
104107
supportedFileServerVRange = mkVersionRange initialXFTPVersion currentXFTPVersion
@@ -124,7 +127,9 @@ data XFTPServerHandshake = XFTPServerHandshake
124127
-- | pub key to agree shared secrets for command authorization and entity ID encryption.
125128
authPubKey :: CertChainPubKey,
126129
-- | signed identity challenge from XFTPClientHello
127-
webIdentityProof :: Maybe C.ASignature
130+
webIdentityProof :: Maybe C.ASignature,
131+
-- | optional server public information (JSON-encoded ServerPublicInfo), sent when version >= serverInfoXFTPVersion
132+
serverInfoBytes :: Maybe ByteString
128133
}
129134

130135
data XFTPClientHandshake = XFTPClientHandshake
@@ -151,13 +156,21 @@ instance Encoding XFTPClientHandshake where
151156
pure XFTPClientHandshake {xftpVersion, keyHash}
152157

153158
instance Encoding XFTPServerHandshake where
154-
smpEncode XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey, webIdentityProof} =
155-
smpEncode (xftpVersionRange, sessionId, authPubKey, C.signatureBytes webIdentityProof)
159+
smpEncode XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey, webIdentityProof, serverInfoBytes} =
160+
smpEncode (xftpVersionRange, sessionId, authPubKey, C.signatureBytes webIdentityProof) <> info
161+
where
162+
info = ifHasServerInfo (maxVersion xftpVersionRange) (smpEncode (Large <$> serverInfoBytes)) ""
156163
smpP = do
157164
(xftpVersionRange, sessionId, authPubKey) <- smpP
158-
webIdentityProof <- optional $ C.decodeSignature <$?> smpP
165+
-- decode the (length-prefixed) signature bytes deterministically: empty bytes decode to Nothing.
166+
-- (Must not use `optional`, which would backtrack and leave the bytes for the parsers that follow.)
167+
webIdentityProof <- C.decodeSignature <$?> smpP
168+
serverInfoBytes <- ifHasServerInfo (maxVersion xftpVersionRange) (unLarge <$$> smpP) (pure Nothing)
159169
Tail _compat <- smpP
160-
pure XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey, webIdentityProof}
170+
pure XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey, webIdentityProof, serverInfoBytes}
171+
172+
ifHasServerInfo :: VersionXFTP -> a -> a -> a
173+
ifHasServerInfo v a b = if v >= serverInfoXFTPVersion then a else b
161174

162175
sendEncFile :: Handle -> (Builder -> IO ()) -> LC.SbState -> Word32 -> IO ()
163176
sendEncFile h send = go

src/Simplex/Messaging/Agent.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ getConnectionRatchetAdHash c = withAgentEnv c . getConnectionRatchetAdHash' c
679679
testProtocolServer :: forall p. ProtocolTypeI p => AgentClient -> NetworkRequestMode -> UserId -> ProtoServerWithAuth p -> IO (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo)))
680680
testProtocolServer c nm userId srv = withAgentEnv' c $ case protocolTypeI @p of
681681
SPSMP -> runSMPServerTest c nm userId srv
682-
SPXFTP -> maybe (Right Nothing) Left <$> runXFTPServerTest c nm userId srv
682+
SPXFTP -> runXFTPServerTest c nm userId srv
683683
SPNTF -> maybe (Right Nothing) Left <$> runNTFServerTest c nm userId srv
684684

685685
-- | set SOCKS5 proxy on/off and optionally set TCP timeouts for fast network

src/Simplex/Messaging/Agent/Client.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,7 +1326,7 @@ runSMPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth sr
13261326
testErr :: ProtocolTestStep -> SMPClientError -> ProtocolTestFailure
13271327
testErr step = ProtocolTestFailure step . protocolClientError SMP addr
13281328

1329-
runXFTPServerTest :: AgentClient -> NetworkRequestMode -> UserId -> XFTPServerWithAuth -> AM' (Maybe ProtocolTestFailure)
1329+
runXFTPServerTest :: AgentClient -> NetworkRequestMode -> UserId -> XFTPServerWithAuth -> AM' (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo)))
13301330
runXFTPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth srv auth) = do
13311331
cfg <- asks $ xftpCfg . config
13321332
g <- asks random
@@ -1352,8 +1352,8 @@ runXFTPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth s
13521352
unless (digest == rcvDigest) $ throwE $ ProtocolTestFailure TSCompareFile $ XFTP (B.unpack $ strEncode srv) DIGEST
13531353
liftError (testErr TSDeleteFile) $ X.deleteXFTPChunk xftp spKey sId
13541354
ok <- netTimeoutInt (tcpTimeout xftpNetworkConfig) nm `timeout` X.closeXFTPClient xftp
1355-
pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok
1356-
Left e -> pure (Just $ testErr TSConnect e)
1355+
pure $ r >> maybe (Left (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const $ Right $ serverInfo (X.thParams xftp)) ok
1356+
Left e -> pure $ Left (testErr TSConnect e)
13571357
where
13581358
addr = B.unpack $ strEncode srv
13591359
testErr :: ProtocolTestStep -> XFTPClientError -> ProtocolTestFailure

tests/AgentTests/FunctionalAPITests.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module AgentTests.FunctionalAPITests
5454
pattern SENT,
5555
agentCfgVPrevPQ,
5656
agentCfgV7,
57+
testServerInformation,
5758
)
5859
where
5960

tests/XFTPAgent.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
module XFTPAgent where
1212

13-
import AgentTests.FunctionalAPITests (get, rfGet, runRight, runRight_, sfGet, withAgent)
13+
import AgentTests.FunctionalAPITests (get, rfGet, runRight, runRight_, sfGet, withAgent, testServerInformation)
1414

1515
import Control.Logger.Simple
1616
import Control.Monad
@@ -83,21 +83,21 @@ xftpAgentTests =
8383
it "if file is expired on server, should report error and continue receiving next file" testXFTPAgentExpiredOnServer
8484
it "should request additional recipient IDs when number of recipients exceeds maximum per request" testXFTPAgentRequestAdditionalRecipientIDs
8585
describe "XFTP server test via agent API" $ do
86-
it "should pass without basic auth" $ \_ -> testXFTPServerTest Nothing (noAuthSrv testXFTPServer2) `shouldReturn` Right Nothing
86+
it "should pass without basic auth" $ \_ -> testXFTPServerTest Nothing (noAuthSrv testXFTPServer2) `shouldReturn` Right (Just (Right testServerInformation))
8787
let srv1 = testXFTPServer2 {keyHash = "1234"}
8888
it "should fail with incorrect fingerprint" $ \_ -> do
8989
testXFTPServerTest Nothing (noAuthSrv srv1) `shouldReturn` Left (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError)
9090
describe "server with password" $ do
9191
let auth = Just "abcd"
9292
srv = ProtoServerWithAuth testXFTPServer2
9393
authErr = ProtocolTestFailure TSCreateFile $ XFTP (B.unpack $ strEncode testXFTPServer2) AUTH
94-
it "should pass with correct password" $ \_ -> testXFTPServerTest auth (srv auth) `shouldReturn` Right Nothing
94+
it "should pass with correct password" $ \_ -> testXFTPServerTest auth (srv auth) `shouldReturn` Right (Just (Right testServerInformation))
9595
it "should fail without password" $ \_ -> testXFTPServerTest auth (srv Nothing) `shouldReturn` Left authErr
9696
it "should fail with incorrect password" $ \_ -> testXFTPServerTest auth (srv $ Just "wrong") `shouldReturn` Left authErr
9797

9898
testXFTPServerTest :: HasCallStack => Maybe BasicAuth -> XFTPServerWithAuth -> IO (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo)))
9999
testXFTPServerTest newFileBasicAuth srv =
100-
withXFTPServerCfg testXFTPServerConfig {newFileBasicAuth, xftpPort = xftpTestPort2} $ \_ ->
100+
withXFTPServerCfg testXFTPServerConfig {newFileBasicAuth, xftpPort = xftpTestPort2, information = Just testServerInformation} $ \_ ->
101101
-- initially passed server is not running
102102
withAgent 1 agentCfg initAgentServers testDB $ \a ->
103103
testProtocolServer a NRMInteractive 1 srv

0 commit comments

Comments
 (0)