5.8 KiB
Simplex.FileTransfer.Server
XFTP server: HTTP/2 request handling, handshake state machine, file operations, and statistics.
Source: FileTransfer/Server.hs
Architecture
The XFTP server runs several concurrent threads via raceAny_:
| Thread | Purpose |
|---|---|
runServer |
HTTP/2 server accepting file transfer requests |
expireFiles |
Periodic file expiration with throttling |
logServerStats |
Periodic stats flush to CSV |
savePrometheusMetrics |
Periodic Prometheus metrics dump |
runCPServer |
Control port for admin commands |
Non-obvious behavior
1. Three-state handshake with session caching
The server maintains a TMap SessionId Handshake with three states:
- No entry: first request — for non-SNI or
xftp-web-hellorequests,processHellogenerates DH key pair and sends server handshake; for SNI requests withoutxftp-web-hello, returnsSESSIONerror HandshakeSent pk: server hello sent, waiting for client handshake with version negotiationHandshakeAccepted thParams: handshake complete, subsequent requests use cached params
Web clients can re-send hello (xftp-web-hello header) even in HandshakeSent or HandshakeAccepted states — the server reuses the existing private key rather than generating a new one.
2. Web identity proof via challenge-response
When a web client sends a hello with a non-empty body, the server parses an XFTPClientHello containing a webChallenge. The server signs challenge <> sessionId with its long-term key and includes the signature in the handshake response. This proves server identity to web clients that cannot verify TLS certificates directly.
3. skipCommitted drains request body on re-upload
If receiveServerFile detects the file is already uploaded (filePath TVar is Just), it cannot simply ignore the request body — the HTTP/2 client would block waiting for the server to consume it. Instead, skipCommitted reads and discards the entire body in fileBlockSize increments, returning FROk when complete. This makes FPUT idempotent from the client's perspective.
4. Atomic quota reservation with rollback
receiveServerFile uses stateTVar to atomically check and reserve storage quota before receiving the file. If the upload fails (timeout, size mismatch, IO error), the reserved size is subtracted from usedStorage and the partial file is deleted. This prevents failed uploads from permanently consuming quota.
5. retryAdd generates new IDs on collision
createFile and addRecipient use retryAdd which generates a random ID and makes up to 3 total attempts (initial + 2 retries) on DUPLICATE_ errors. This handles the astronomically unlikely case of random ID collision without requiring uniqueness checking before insertion.
6. Timing attack mitigation on entity lookup
verifyXFTPTransmission calls dummyVerifyCmd (imported from SMP server) when a file entity is not found. This equalizes response timing to prevent attackers from distinguishing "entity doesn't exist" from "signature invalid" based on latency.
7. BLOCKED vs EntityOff distinction
When verifyXFTPTransmission reads fileStatus:
EntityActive→ proceed with commandEntityBlocked info→ returnBLOCKEDwith blocking reasonEntityOff→ returnAUTH(same as entity-not-found)
EntityOff is treated identically to missing entities for information-hiding purposes.
8. blockServerFile deletes the physical file
Despite the name suggesting it only marks a file as blocked, blockServerFile also deletes the physical file from disk via deleteOrBlockServerFile_. The deleted = True parameter to blockFile in the store adjusts usedStorage. A blocked file returns BLOCKED errors on access but has no data on disk.
9. Stats restore overrides counts from live store
restoreServerStats loads stats from the backup file but overrides _filesCount and _filesSize with values computed from the live file store (TMap size and usedStorage TVar). If the backup values differ, warnings are logged. This handles cases where files were expired or deleted while the server was down.
10. File expiration with configurable throttling
expireServerFiles accepts an optional itemDelay (100ms when called from the periodic thread, Nothing at startup). Between each file check, threadDelay itemDelay prevents expiration from monopolizing IO. At startup, files are expired without delay to clean up quickly.
11. Stats log aligns to wall-clock midnight
logServerStats computes an initialDelay to align the first stats flush to logStatsStartTime (default 0 = midnight UTC). If the target time already passed today, it adds 86400 seconds for the next day. Subsequent flushes use exact logInterval cadence.
12. Physical file deleted before store cleanup
deleteOrBlockServerFile_ removes the physical file first, then runs the STM store action. If the process crashes between these two operations, the store will reference a file that no longer exists on disk. The next access would return AUTH (file not found on disk), and eventual expiration would clean the store entry.
13. SNI-dependent CORS and web serving
CORS headers require both sniUsed = True and addCORSHeaders = True in the transport config. Static web page serving is enabled when sniUsed = True. Non-SNI connections (direct TLS without hostname) skip both CORS and web serving. This separates the web-facing and protocol-facing behaviors of the same port.
14. Control port file operations use recipient index
CPDelete and CPBlock commands look up files via getFile fs SFRecipient fileId, meaning the control port takes a recipient ID, not a sender ID. This is the ID visible to recipients and contained in file descriptions.