mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-06-02 02:24:29 +00:00
xftp specs
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# Simplex.FileTransfer.Agent
|
||||
|
||||
> XFTP agent: worker-based file send/receive/delete with retry, encryption, redirect chains, and file description generation.
|
||||
|
||||
**Source**: [`FileTransfer/Agent.hs`](../../../../src/Simplex/FileTransfer/Agent.hs)
|
||||
|
||||
## Architecture
|
||||
|
||||
The XFTP agent uses five worker types organized in three categories:
|
||||
|
||||
| Worker | Key (server) | Purpose |
|
||||
|--------|-------------|---------|
|
||||
| `xftpRcvWorker` | `Just server` | Download chunks from a specific XFTP server |
|
||||
| `xftpRcvLocalWorker` | `Nothing` | Decrypt completed downloads locally |
|
||||
| `xftpSndPrepareWorker` | `Nothing` | Encrypt files and create chunks on servers |
|
||||
| `xftpSndWorker` | `Just server` | Upload chunks to a specific XFTP server |
|
||||
| `xftpDelWorker` | `Just server` | Delete chunks from a specific XFTP server |
|
||||
|
||||
Workers are created on-demand via `getAgentWorker` and keyed by server address. The local workers (keyed by `Nothing`) handle CPU-bound operations that don't require network access.
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. startXFTPWorkers vs startXFTPSndWorkers
|
||||
|
||||
`startXFTPWorkers` starts all three worker categories (rcv, snd, del). `startXFTPSndWorkers` starts only snd workers. This distinction exists because receiving and deleting require a full agent context, while sending can operate with a partial setup (used when the agent is in send-only mode).
|
||||
|
||||
### 2. Download completion triggers local worker
|
||||
|
||||
When `downloadFileChunk` determines that all chunks are received (`all chunkReceived chunks`), it calls `getXFTPRcvWorker True c Nothing` to wake the local decryption worker. The `True` parameter signals that work is available. Without this, the local worker would sleep until the next `waitForWork` check.
|
||||
|
||||
### 3. Decryption verifies both digest and size before decrypting
|
||||
|
||||
`decryptFile` first computes the total size of all encrypted chunk files, then their SHA-512 digest. If either mismatches the expected values, it throws an error *before* starting decryption. This prevents wasting CPU on corrupted or tampered downloads.
|
||||
|
||||
### 4. Redirect chain with depth limit
|
||||
|
||||
When a received file has a `redirect`, the local worker:
|
||||
1. Decrypts the redirect file (a YAML file description)
|
||||
2. Validates the inner description's size and digest against `RedirectFileInfo`
|
||||
3. Registers the inner file's chunks and starts downloading them
|
||||
|
||||
The redirect chain is implicitly limited to depth 1: `createRcvFileRedirect` creates the destination file entry with `redirect = Nothing`, and `updateRcvFileRedirect` does not update the redirect column. So even if the decoded inner description contains a redirect field, the database record for the destination file has no redirect, preventing further chaining.
|
||||
|
||||
### 5. Decrypting worker resumes from RFSDecrypting
|
||||
|
||||
If the agent restarts while a file is in `RFSDecrypting` status, the local worker detects this and deletes the partially-decrypted output file before restarting decryption. This prevents corrupted output from a previous incomplete decryption attempt.
|
||||
|
||||
### 6. Encryption worker resumes from SFSEncrypting
|
||||
|
||||
Similarly, `prepareFile` checks `status /= SFSEncrypted` and deletes the partial encrypted file if status is `SFSEncrypting`. This allows clean restart of interrupted encryption.
|
||||
|
||||
### 7. Redirect files must be single-chunk
|
||||
|
||||
`encryptFileForUpload` for redirect files calls `singleChunkSize` instead of `prepareChunkSizes`. If the redirect file description doesn't fit in a single chunk, it throws `FILE SIZE`. This ensures redirect files are atomic — they either download completely or not at all.
|
||||
|
||||
### 8. addRecipients recursive batching
|
||||
|
||||
During upload, `addRecipients` recursively calls itself if a chunk needs more recipients than `xftpMaxRecipientsPerRequest`. Each iteration sends an FADD command for up to `maxRecipients` new recipients, accumulates the results, and recurses until all recipients are registered.
|
||||
|
||||
### 9. File description generation cross-product
|
||||
|
||||
`createRcvFileDescriptions` (in both `Agent.hs` and `Client/Main.hs`) performs a cross-product transformation: M chunks × R replicas × N recipients → N file descriptions, each containing M chunks with R replicas. The `addRcvChunk` accumulator builds a `Map rcvNo (Map chunkNo FileChunk)` to correctly distribute replicas across recipient descriptions.
|
||||
|
||||
### 10. withRetryIntervalLimit caps consecutive retries
|
||||
|
||||
`withRetryIntervalLimit maxN` allows at most `maxN` total attempts (initial attempt at `n=0` plus `maxN-1` retries). When all attempts are exhausted for temporary errors, the operation is silently abandoned for this work cycle — the chunk remains in pending state and may be retried on the next cycle. Only permanent errors (handled by `retryDone`) mark the file as errored.
|
||||
|
||||
### 11. Retry distinguishes temporary from permanent errors
|
||||
|
||||
`retryOnError` checks `temporaryOrHostError`: temporary/host errors trigger retry with exponential backoff; permanent errors (AUTH, SIZE, etc.) immediately mark the file as failed. On host errors during retry, a warning notification is sent to the client.
|
||||
|
||||
### 12. Delete workers skip files older than rcvFilesTTL
|
||||
|
||||
`runXFTPDelWorker` uses `rcvFilesTTL` (not a dedicated delete TTL) to filter pending deletions. Files older than this TTL would already be expired on the server, so attempting deletion is pointless. This reuses the receive TTL as a proxy for server-side expiration.
|
||||
|
||||
### 13. closeXFTPAgent atomically swaps worker maps
|
||||
|
||||
`closeXFTPAgent` uses `swapTVar workers M.empty` to atomically replace each worker map with an empty map, then cancels all retrieved workers. This prevents races where a new worker could be inserted between reading and clearing the map.
|
||||
|
||||
### 14. assertAgentForeground dual check
|
||||
|
||||
`assertAgentForeground` both throws if the agent is inactive (`throwWhenInactive`) and blocks until it's in the foreground (`waitUntilForeground`). This is called before every chunk operation to ensure the agent isn't suspended or backgrounded during file transfers.
|
||||
|
||||
### 15. Per-server stats tracking
|
||||
|
||||
Every chunk download, upload, and delete operation increments per-server statistics (`downloads`, `uploads`, `deletions`, `downloadAttempts`, `uploadAttempts`, `deleteAttempts`, and error variants). Size-based stats (`downloadsSize`, `uploadsSize`) track throughput in kilobytes.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Simplex.FileTransfer.Client
|
||||
|
||||
> XFTP client: connection management, handshake, chunk upload/download with forward secrecy.
|
||||
|
||||
**Source**: [`FileTransfer/Client.hs`](../../../../src/Simplex/FileTransfer/Client.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. ALPN-based handshake version selection
|
||||
|
||||
`getXFTPClient` checks the ALPN result after TLS negotiation:
|
||||
- **`xftpALPNv1` or `httpALPN11`**: performs v1 handshake with key exchange (`httpALPN11` is used for web port connections)
|
||||
- **No ALPN or unrecognized**: uses legacy v1 transport parameters without handshake
|
||||
|
||||
### 2. Server certificate chain validation
|
||||
|
||||
`xftpClientHandshakeV1` validates the server's identity by checking that the CA fingerprint from the certificate chain matches the expected `keyHash` from the server address. The server signs an authentication public key (X25519) with its long-term key. The client verifies this signature against the certificate chain, then extracts the X25519 key for HMAC-based command authentication. This authentication key is distinct from the per-download ephemeral DH keys.
|
||||
|
||||
### 3. Ephemeral DH key pair per download
|
||||
|
||||
`downloadXFTPChunk` generates a fresh X25519 key pair for each chunk download. The public key is sent with the FGET command; the server responds with its own ephemeral key. The derived shared secret encrypts the file data in transit. This provides forward secrecy — compromising a past DH key doesn't decrypt other downloads.
|
||||
|
||||
### 4. Chunk-size-proportional download timeout
|
||||
|
||||
`downloadXFTPChunk` calculates the timeout as `baseTimeout + (sizeInKB * perKbTimeout)`, where `baseTimeout` is the base TCP timeout and `perKbTimeout` is a per-kilobyte timeout from the network config. Larger chunks get proportionally more time. This prevents premature timeouts on large chunks over slow connections.
|
||||
|
||||
### 5. prepareChunkSizes threshold algorithm
|
||||
|
||||
`prepareChunkSizes` selects chunk sizes using a 75% threshold: if the remaining payload exceeds 75% of the next larger chunk size, it uses the larger size. Otherwise, it uses the smaller size. `singleChunkSize` returns `Just size` only if the payload fits in a single chunk (used for redirect files which must be single-chunk).
|
||||
|
||||
### 6. Upload sends file body after command response
|
||||
|
||||
`uploadXFTPChunk` sends the FPUT command and file body in the same streaming HTTP/2 request: the protocol command block is sent first, followed immediately by the raw file data via `hSendFile`. The server response (`FROk` or error) is received only after both the command and file body have been fully sent. This is a single HTTP/2 round trip, not a two-phase interaction.
|
||||
|
||||
### 7. Empty corrId as nonce
|
||||
|
||||
`sendXFTPCommand` uses `""` (empty bytestring) as the correlation ID for all commands. XFTP is strictly request-response within a single HTTP/2 stream, so correlation IDs are unnecessary. The empty value is passed to `C.cbNonce` to produce a constant nonce for command authentication (HMAC/signing), not encryption — XFTP authenticates commands but does not encrypt them within the TLS tunnel.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Simplex.FileTransfer.Client.Agent
|
||||
|
||||
> XFTP client connection management with TMVar-based sharing, async retry, and connection lifecycle.
|
||||
|
||||
**Source**: [`FileTransfer/Client/Agent.hs`](../../../../../src/Simplex/FileTransfer/Client/Agent.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. TMVar-based connection sharing
|
||||
|
||||
`getXFTPServerClient` first checks the `TMap XFTPServer (TMVar (Either XFTPClientAgentError XFTPClient))`. If no entry exists, it atomically inserts an empty `TMVar` and initiates connection. Other threads requesting the same server block on `readTMVar` until the connection is established or fails. This prevents duplicate connections to the same server.
|
||||
|
||||
### 2. Async retry on temporary errors
|
||||
|
||||
When `newXFTPClient` encounters a temporary error, it launches an async retry loop that attempts reconnection with backoff. The `TMVar` remains in the map but is empty until the retry succeeds. Other threads waiting on `readTMVar` block until either the retry succeeds or a permanent error occurs.
|
||||
|
||||
### 3. Permanent error cleanup
|
||||
|
||||
On permanent error, `newXFTPClient` puts the `Left error` into the `TMVar` (unblocking waiters) AND deletes the entry from the `TMap`. This means the next caller will see no entry and create a fresh connection attempt, rather than reading a stale error. Waiters that already read the `Left` receive the error.
|
||||
|
||||
### 4. Connection timeout
|
||||
|
||||
`waitForXFTPClient` wraps `readTMVar` in a timeout. If the connection establishment takes too long (e.g., server unreachable and retry loop is slow), the caller gets a timeout error rather than blocking indefinitely. The underlying connection attempt continues in the background.
|
||||
|
||||
### 5. closeXFTPServerClient removes from TMap
|
||||
|
||||
Closing a server client deletes its entry from the TMap, so the next request will establish a fresh connection. This is called on connection errors during file operations to force reconnection.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Simplex.FileTransfer.Client.Main
|
||||
|
||||
> XFTP CLI client: send, receive, delete files with parallel chunk operations and web URI encoding.
|
||||
|
||||
**Source**: [`FileTransfer/Client/Main.hs`](../../../../../src/Simplex/FileTransfer/Client/Main.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Web URI encoding: base64url(deflate(YAML))
|
||||
|
||||
`encodeWebURI` compresses the YAML-encoded file description with raw DEFLATE, then base64url-encodes the result. `decodeWebURI` reverses this. The compressed description goes in the URL fragment (after `#`), which is never sent to the server — the file description stays client-side.
|
||||
|
||||
### 2. CLI receive accepts both file paths and URLs
|
||||
|
||||
`getInputFileDescription` checks if the input starts with `http://` or `https://`. If so, it extracts the URL fragment, decodes it via `decodeWebURI`, and uses the resulting file description. Otherwise, it reads a YAML file from disk. This allows receiving files via web links without a browser.
|
||||
|
||||
### 3. Redirect chain depth limited to 1
|
||||
|
||||
`receive` tracks a `depth` parameter starting at 1. After following one redirect, `depth` becomes 0. A second redirect throws "Redirect chain too long". This prevents infinite redirect loops from malicious file descriptions.
|
||||
|
||||
### 4. Parallel chunk uploads with server grouping
|
||||
|
||||
`uploadFile` groups chunks by server via `groupAllOn`, then uses `pooledForConcurrentlyN 16` to process up to 16 server-groups concurrently. Within each group, chunks are uploaded sequentially (`mapM`). Errors from any chunk are collected and the first one is thrown.
|
||||
|
||||
### 5. Random server selection
|
||||
|
||||
`getXFTPServer` selects a random server from the provided list for each chunk. With a single server, it's deterministic. With multiple servers, it uses `StdGen` in a TVar for thread-safe random selection via `stateTVar`.
|
||||
|
||||
### 6. withReconnect nests retry with reconnection
|
||||
|
||||
`withReconnect` wraps `withRetry` twice: the outer retry reconnects to the server, and the inner operation runs against the connection. On failure, the server connection is explicitly closed before retrying, forcing a fresh connection on the next attempt.
|
||||
|
||||
### 7. withRetry rejects zero retries
|
||||
|
||||
`withRetry' 0` returns an "internal: no retry attempts" error. `withRetry' 1` executes the action once without retry. This off-by-one convention means `retryCount = 3` (the default) gives 3 total attempts (1 initial + 2 retries).
|
||||
|
||||
### 8. File description auto-deletion prompt
|
||||
|
||||
After successful receive or delete, `removeFD` either auto-deletes the file description (if `--yes` flag) or prompts the user. This prevents accidental reuse of one-time file descriptions — each receive consumes the description by ACKing chunks on the server.
|
||||
|
||||
### 9. Sender description uses first replica's server
|
||||
|
||||
`createSndFileDescription` takes the server from the first replica of each chunk for the sender's `FileChunkReplica`. This reflects the current limitation that each chunk is uploaded to exactly one server — the sender description records that single server.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Simplex.FileTransfer.Crypto
|
||||
|
||||
> File encryption and decryption with streaming, padding, and auth tag verification.
|
||||
|
||||
**Source**: [`FileTransfer/Crypto.hs`](../../../../src/Simplex/FileTransfer/Crypto.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Embedded file header in encrypted stream
|
||||
|
||||
`encryptFile` prepends the `FileHeader` (containing filename and optional `fileExtra`) to the plaintext before encryption. A total data size field (8 bytes, `fileSizeLen`) is prepended before the header, encoding the combined size of header + file content. The decryptor uses this to distinguish real data from padding. The recipient must parse the header after decryption to recover the original filename — the header is not transmitted separately.
|
||||
|
||||
### 2. Fixed-size padding hides actual file size
|
||||
|
||||
The encrypted output is padded to `encSize` (the sum of chunk sizes). Since chunk sizes are fixed powers of 2 (64KB, 256KB, 1MB, 4MB), the encrypted file size reveals only which chunk size bucket the file falls into, not the actual size. The encryption streams data with `LC.sbEncryptChunk` in a loop, pads the remaining space, then manually appends the auth tag via `LC.sbAuth`. This manual streaming approach (rather than using the all-at-once `LC.sbEncryptTailTag`) is necessary because encryption is interleaved with file I/O.
|
||||
|
||||
### 3. Dual decrypt paths: single-chunk vs multi-chunk
|
||||
|
||||
`decryptChunks` takes different paths based on chunk count:
|
||||
- **Single chunk**: reads the entire file into memory via `LB.readFile`, decrypts in-memory with `LC.sbDecryptTailTag`
|
||||
- **Multiple chunks**: opens the destination file for writing and streams through each chunk file with `LC.sbDecryptChunkLazy` (lazy bytestring variant), verifying the auth tag from the final chunk
|
||||
|
||||
The single-chunk path avoids file handle management overhead for small files.
|
||||
|
||||
### 4. Auth tag failure deletes output file
|
||||
|
||||
In the multi-chunk streaming path, if `BA.constEq` detects an auth tag mismatch after decrypting all chunks, the partially-written output file is deleted before returning `FTCEInvalidAuthTag`. This prevents consumers from using a file whose integrity is unverified.
|
||||
|
||||
### 5. Streaming encryption uses 64KB blocks
|
||||
|
||||
`encryptFile` reads plaintext in 65536-byte blocks (`LC.sbEncryptChunk`), regardless of the XFTP chunk size. These are encryption blocks within a single continuous stream — not to be confused with XFTP protocol chunks which are much larger (64KB–4MB).
|
||||
@@ -0,0 +1,43 @@
|
||||
# Simplex.FileTransfer.Description
|
||||
|
||||
> File description: YAML encoding/decoding, validation, URI format, and replica optimization.
|
||||
|
||||
**Source**: [`FileTransfer/Description.hs`](../../../../src/Simplex/FileTransfer/Description.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. ValidFileDescription non-exported constructor
|
||||
|
||||
`ValidFileDescription` is a newtype with a non-exported data constructor (`ValidFD`), but the module exports a bidirectional pattern synonym `ValidFileDescription` that can be used as a constructor. Despite this, `validateFileDescription` provides the canonical validation path, checking:
|
||||
- Chunk numbers are sequential starting from 1
|
||||
- Total chunk sizes equal the declared file size
|
||||
|
||||
Note: an empty chunk list with size 0 passes validation — there is no explicit "at least one chunk" check.
|
||||
|
||||
### 2. First-replica-only digest and chunkSize
|
||||
|
||||
When encoding chunks to YAML via `unfoldChunksToReplicas`, the `digest` and non-default `chunkSize` fields are only included on the first replica of each chunk. Subsequent replicas of the same chunk omit these fields. `foldReplicasToChunks` reconstructs them by carrying forward the digest/size from the first replica. If replicas have conflicting digests or sizes, validation fails.
|
||||
|
||||
### 3. Default chunkSize elision
|
||||
|
||||
The top-level `FileDescription` has a `chunkSize` field. Individual chunk replicas only serialize their `chunkSize` if it differs from this default. This saves space in the common case where most chunks are the same size (only the last chunk may be smaller).
|
||||
|
||||
### 4. YAML encoding groups replicas by server
|
||||
|
||||
`groupReplicasByServer` groups all chunk replicas by their server, producing `FileServerReplica` records. This is the serialization format — replicas are organized by server, not by chunk. The parser (`foldReplicasToChunks`) reverses this grouping back to per-chunk replica lists.
|
||||
|
||||
### 5. FileDescriptionURI uses query-string encoding
|
||||
|
||||
`FileDescriptionURI` serializes file descriptions into a compact query-string format (key=value pairs separated by `&`) with `QEscape` encoding for binary values. This is distinct from the YAML format used for file-based descriptions. The URI format is designed for embedding in links.
|
||||
|
||||
### 6. QR code size limit
|
||||
|
||||
`qrSizeLimit = 1002` bytes limits the maximum size of a file description URI that can be encoded as a QR code. Descriptions exceeding this limit cannot be shared via QR code and require alternative transport.
|
||||
|
||||
### 7. Soft and hard file size limits
|
||||
|
||||
Two limits exist: `maxFileSize = 1GB` (soft limit, checked by CLI client) and `maxFileSizeHard = 5GB` (hard limit, checked during agent-side encryption). The soft limit is a user-facing guard; the hard limit prevents resource exhaustion during encryption.
|
||||
|
||||
### 8. Redirect file descriptions
|
||||
|
||||
A `FileDescription` can contain a `redirect` field pointing to another file's metadata (`RedirectFileInfo` with size and digest). The outer description downloads an encrypted YAML file that, once decrypted, yields the actual `FileDescription` for the real file. This adds one level of indirection for privacy — the relay servers hosting the redirect don't know the actual file's servers.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Simplex.FileTransfer.Protocol
|
||||
|
||||
> XFTP protocol types, commands, responses, and credential verification.
|
||||
|
||||
**Source**: [`FileTransfer/Protocol.hs`](../../../../src/Simplex/FileTransfer/Protocol.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Asymmetric credential checks by command
|
||||
|
||||
`checkCredentials` enforces different rules per command:
|
||||
- **FNEW**: requires `auth` (signature) but must NOT have a `fileId` — the sender key from the command body is used for verification
|
||||
- **PING**: must have NEITHER `auth` NOR `fileId` — actively rejects their presence
|
||||
- **All others** (FADD, FPUT, FDEL, FGET, FACK): require both `fileId` AND auth key
|
||||
|
||||
This asymmetry means FNEW and PING bypass the standard entity-lookup path entirely — they are handled as separate `XFTPRequest` constructors (`XFTPReqNew`, `XFTPReqPing`).
|
||||
|
||||
### 2. BLOCKED response downgraded to AUTH for old clients
|
||||
|
||||
`encodeProtocol` checks the protocol version: if `v < blockedFilesXFTPVersion`, a `BLOCKED` response is encoded as `AUTH` instead. This prevents old clients that don't understand `BLOCKED` from receiving an unknown error type. The blocking information is silently lost for these clients.
|
||||
|
||||
### 3. Single-transmission batch enforcement
|
||||
|
||||
`xftpDecodeTServer` calls `xftpDecodeTransmission` which rejects batches containing more than one transmission. Despite using the batch framing format (length-prefixed), XFTP requires exactly one command per request. This differs from SMP where true batching is supported.
|
||||
|
||||
### 4. xftpEncodeBatch1 always uses batch framing
|
||||
|
||||
Even for single transmissions, `xftpEncodeBatch1` wraps the encoded transmission in batch format (1-byte count prefix + 2-byte length-prefixed transmission). There is no "non-batch" mode in XFTP — all protocol messages use the batch wire format regardless of the negotiated version.
|
||||
|
||||
### 5. FileParty GADT partitions command space
|
||||
|
||||
Commands are indexed by `FileParty` (`SFSender` / `SFRecipient`) at the type level via `FileCmd`. This ensures at compile time that sender commands (FNEW, FADD, FPUT, FDEL) and recipient commands (FGET, FACK, PING) cannot be confused. The server pattern-matches on `SFileParty` to determine which index (sender vs recipient) to look up in the file store.
|
||||
|
||||
### 6. Empty corrId and implicit session ID
|
||||
|
||||
`sendXFTPCommand` in the client uses an empty bytestring as `corrId`. This empty value is passed to `C.cbNonce` to produce a constant nonce for command authentication (HMAC/signing). With `implySessId = False` in the default XFTP transport setup, the session ID is not prepended to entity IDs during parsing. Session identity is provided by the TLS connection itself.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Simplex.FileTransfer.Server
|
||||
|
||||
> XFTP server: HTTP/2 request handling, handshake state machine, file operations, and statistics.
|
||||
|
||||
**Source**: [`FileTransfer/Server.hs`](../../../../src/Simplex/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-hello` requests, `processHello` generates DH key pair and sends server handshake; for SNI requests without `xftp-web-hello`, returns `SESSION` error
|
||||
- **`HandshakeSent pk`**: server hello sent, waiting for client handshake with version negotiation
|
||||
- **`HandshakeAccepted 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 command
|
||||
- `EntityBlocked info` → return `BLOCKED` with blocking reason
|
||||
- `EntityOff` → return `AUTH` (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.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Simplex.FileTransfer.Server.Env
|
||||
|
||||
> XFTP server environment: configuration, storage quota tracking, and request routing.
|
||||
|
||||
**Source**: [`FileTransfer/Server/Env.hs`](../../../../../src/Simplex/FileTransfer/Server/Env.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Startup storage accounting with quota warning
|
||||
|
||||
`newXFTPServerEnv` computes `usedStorage` by summing file sizes from the in-memory store at startup. If the computed usage exceeds the configured `fileSizeQuota`, a warning is logged but the server still starts. This allows the server to come up even if it's over quota (e.g., after a quota reduction), relying on expiration to reclaim space.
|
||||
|
||||
### 2. XFTPRequest ADT separates new files from commands
|
||||
|
||||
`XFTPRequest` has three constructors:
|
||||
- `XFTPReqNew`: file creation (carries `FileInfo`, recipient keys, optional basic auth)
|
||||
- `XFTPReqCmd`: command on an existing file (carries file ID, `FileRec`, and the command)
|
||||
- `XFTPReqPing`: health check
|
||||
|
||||
This separation occurs after credential verification in `Server.hs`. `XFTPReqNew` bypasses entity lookup entirely since the file doesn't exist yet.
|
||||
|
||||
### 3. fileTimeout for upload deadline
|
||||
|
||||
`fileTimeout` in `XFTPServerConfig` sets the maximum time allowed for a single file upload (FPUT). The server wraps the receive operation in `timeout fileTimeout`. Default is 5 minutes (for 4MB chunks). This prevents slow or stalled uploads from holding server resources indefinitely.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Simplex.FileTransfer.Server.Main
|
||||
|
||||
> XFTP server CLI: INI configuration parsing, TLS setup, and default constants.
|
||||
|
||||
**Source**: [`FileTransfer/Server/Main.hs`](../../../../../src/Simplex/FileTransfer/Server/Main.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Key server constants
|
||||
|
||||
| Constant | Value | Purpose |
|
||||
|----------|-------|---------|
|
||||
| `fileIdSize` | 16 bytes | Random file/recipient ID length |
|
||||
| `fileTimeout` | 5 minutes | Maximum upload duration per chunk |
|
||||
| `logStatsInterval` | 86400s (daily) | Stats CSV flush interval |
|
||||
| `logStatsStartTime` | 0 (midnight UTC) | First stats flush time-of-day |
|
||||
|
||||
### 2. allowedChunkSizes defaults to all four sizes
|
||||
|
||||
If not configured, `allowedChunkSizes` defaults to `[kb 64, kb 256, mb 1, mb 4]`. The INI file can restrict this to a subset, controlling which chunk sizes the server accepts.
|
||||
|
||||
### 3. Storage quota from INI with unit parsing
|
||||
|
||||
`fileSizeQuota` is parsed from the INI `[STORE_LOG]` section using `FileSize` parsing, which accepts byte values with optional unit suffixes (KB, MB, GB). Absence means unlimited quota (`Nothing`).
|
||||
|
||||
### 4. Dual TLS credential support
|
||||
|
||||
The server supports both primary TLS credentials (`caCertificateFile`/`certificateFile`/`privateKeyFile`) and optional HTTP-specific credentials (`httpCaCertificateFile`/etc.). When HTTP credentials are present, the server uses `defaultSupportedParamsHTTPS` which enables broader TLS compatibility for web clients.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Simplex.FileTransfer.Server.Stats
|
||||
|
||||
> XFTP server statistics: IORef-based counters with backward-compatible persistence.
|
||||
|
||||
**Source**: [`FileTransfer/Server/Stats.hs`](../../../../../src/Simplex/FileTransfer/Server/Stats.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. setFileServerStats is not thread safe
|
||||
|
||||
`setFileServerStats` directly writes to IORefs without synchronization. It is explicitly intended for server startup only (restoring from backup file), before any concurrent threads are running.
|
||||
|
||||
### 2. Backward-compatible parsing
|
||||
|
||||
The `strP` parser uses `opt` for newer fields, defaulting missing fields to 0. This allows reading stats files from older server versions that don't include fields like `filesBlocked` or `fileDownloadAcks`.
|
||||
|
||||
### 3. PeriodStats for download tracking
|
||||
|
||||
`filesDownloaded` uses `PeriodStats` (not a simple `IORef Int`) to track unique file downloads over time periods (day/week/month). This enables the CSV stats log to report distinct files downloaded per period, not just total download count.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Simplex.FileTransfer.Server.Store
|
||||
|
||||
> STM-based in-memory file store with dual indices, storage accounting, and privacy-preserving expiration.
|
||||
|
||||
**Source**: [`FileTransfer/Server/Store.hs`](../../../../../src/Simplex/FileTransfer/Server/Store.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Dual-index lookup by sender and recipient
|
||||
|
||||
The file store maintains two indices: `files :: TMap SenderId FileRec` (by sender ID) and `recipients :: TMap RecipientId (SenderId, RcvPublicAuthKey)` (by recipient ID, storing the sender ID and the recipient's public auth key). `getFile` dispatches on `SFileParty`: sender lookups use `files` directly, recipient lookups use `recipients` to find the `SenderId` then look up the `FileRec` in `files`. This means recipient operations require two TMap lookups.
|
||||
|
||||
### 2. addRecipient checks both inner Set and global TMap
|
||||
|
||||
`addRecipient` first checks the per-file `recipientIds` Set for duplicates, then inserts into the global `recipients` TMap. If either has a collision, it returns `DUPLICATE_`. The dual check is necessary because the Set tracks per-file membership while the TMap enforces global uniqueness of recipient IDs.
|
||||
|
||||
### 3. Storage accounting on upload completion
|
||||
|
||||
`setFilePath` adds the file size to `usedStorage` and records the file path in the `filePath` TVar. However, during normal FPUT handling, `Server.hs` does NOT call `setFilePath` — it directly writes `filePath` via `writeTVar`. The quota reservation in `Server.hs` (`stateTVar` on `usedStorage`) is the sole `usedStorage` increment during upload. `setFilePath` IS called during store log replay (`StoreLog.hs`), where it increments `usedStorage`; `newXFTPServerEnv` then overwrites with the correct value computed from the live store.
|
||||
|
||||
### 4. deleteFile removes all recipients atomically
|
||||
|
||||
`deleteFile` atomically removes the sender entry from `files`, all recipient entries from the global `recipients` TMap, and unconditionally subtracts the file size from `usedStorage` (regardless of whether the file was actually uploaded). The entire operation runs in a single STM transaction.
|
||||
|
||||
### 5. RoundedSystemTime for privacy-preserving expiration
|
||||
|
||||
File timestamps use `RoundedFileTime` which is `RoundedSystemTime 3600` — system time rounded to 1-hour precision. This means files created within the same hour have identical timestamps. An observer with access to the store cannot determine exact file creation times, only the hour.
|
||||
|
||||
### 6. expiredFilePath returns path only if expired
|
||||
|
||||
`expiredFilePath` returns `STM (Maybe (Maybe FilePath))`. The outer `Maybe` is `Nothing` when the file doesn't exist or isn't expired; the inner `Maybe` is the file path (present only if the file was uploaded). The expiration check adds `fileTimePrecision` (one hour) to the creation timestamp before comparing, providing a grace period. The caller uses the inner path to decide whether to also delete the physical file.
|
||||
|
||||
### 7. ackFile removes single recipient
|
||||
|
||||
`ackFile` removes a specific recipient from both the global `recipients` TMap and the per-file `recipientIds` Set. Unlike `deleteFile` which removes the entire file, `ackFile` only removes one recipient's access. The file and other recipients remain intact.
|
||||
|
||||
### 8. blockFile conditional storage adjustment
|
||||
|
||||
`blockFile` takes a `deleted :: Bool` parameter. When `True` (file blocked with physical deletion), it subtracts the file size from `usedStorage`. When `False` (block without deletion), storage is unchanged. This allows blocking without physical deletion for audit purposes. Currently, both the server's `blockServerFile` and the store log replay path pass `True`.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Simplex.FileTransfer.Server.StoreLog
|
||||
|
||||
> Append-only store log for XFTP file operations with error-resilient replay and compaction.
|
||||
|
||||
**Source**: [`FileTransfer/Server/StoreLog.hs`](../../../../../src/Simplex/FileTransfer/Server/StoreLog.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Error-resilient replay
|
||||
|
||||
`readFileStore` parses the store log line-by-line. Lines that fail to parse or fail to process (e.g., referencing a nonexistent sender ID) are logged as errors but do not halt replay. The store is reconstructed from whatever valid entries exist. This allows the server to recover from partial log corruption.
|
||||
|
||||
### 2. Sender ID validation on recipient writes
|
||||
|
||||
`writeFileStore` during compaction validates that each recipient's sender ID in the `recipients` TMap matches the `senderId` of the corresponding `FileRec`. This guards against in-memory state corruption (e.g., if a bug caused the `recipients` TMap and `FileRec.recipientIds` to get out of sync), not log corruption — the validation happens before writing the compacted log.
|
||||
|
||||
### 3. Backward-compatible status parsing
|
||||
|
||||
`AddFile` log entries include an `EntityStatus` field. The parser uses `<|> pure EntityActive` as a fallback, defaulting to `EntityActive` when the status field is missing. This allows reading store logs from older server versions that didn't record entity status.
|
||||
|
||||
### 4. Compaction on restart
|
||||
|
||||
`readFileStore` replays the full log to rebuild the in-memory store. The caller (in `Server/Env.hs`) then writes a fresh, compacted store log containing only the current state. This eliminates deleted entries and redundant operations, keeping the log size proportional to active state rather than total history.
|
||||
|
||||
### 5. Log entry types track operation lifecycle
|
||||
|
||||
Six log entry types capture the complete file lifecycle:
|
||||
- `AddFile`: file creation with sender ID, file info, timestamp, and status
|
||||
- `AddRecipients`: recipient registration (batched as `NonEmpty FileRecipient`) with sender ID association
|
||||
- `PutFile`: upload completion with file path
|
||||
- `DeleteFile`: file deletion by sender ID
|
||||
- `AckFile`: single recipient acknowledgment
|
||||
- `BlockFile`: file blocking with blocking info
|
||||
@@ -0,0 +1,27 @@
|
||||
# Simplex.FileTransfer.Types
|
||||
|
||||
> Agent-side file transfer types: receive/send file records, status state machines, chunk/replica structures.
|
||||
|
||||
**Source**: [`FileTransfer/Types.hs`](../../../../src/Simplex/FileTransfer/Types.hs)
|
||||
|
||||
## Non-obvious behavior
|
||||
|
||||
### 1. Receive file status state machine
|
||||
|
||||
`RcvFileStatus` progresses: `RFSReceiving` → `RFSReceived` → `RFSDecrypting` → `RFSComplete`, with `RFSError` as a terminal state reachable from any non-complete state. The `RFSReceived` → `RFSDecrypting` transition is significant: all chunks are downloaded but decryption hasn't started. The local worker (server=Nothing) picks up files in `RFSReceived` status.
|
||||
|
||||
### 2. Send file status state machine
|
||||
|
||||
`SndFileStatus` progresses: `SFSNew` → `SFSEncrypting` → `SFSEncrypted` → `SFSUploading` → `SFSComplete`, with `SFSError` as terminal. The prepare worker handles `SFSNew` → `SFSEncrypted` (including retry from `SFSEncrypting`), while per-server upload workers handle `SFSUploading` → `SFSComplete`.
|
||||
|
||||
### 3. Encrypted file path convention
|
||||
|
||||
`sndFileEncPath` constructs the path as `prefixPath </> "xftp.encrypted"`. This is a convention shared between the agent (`Agent.hs`) and this module — both must agree on where the encrypted intermediate file lives relative to the prefix directory.
|
||||
|
||||
### 4. FileHeader fileExtra for future extension
|
||||
|
||||
`FileHeader` contains `fileName` and an optional `fileExtra :: Maybe Text` field. Currently unused (`Nothing` in all callers), it provides a forward-compatible extension point embedded in the encrypted file header without requiring protocol version changes.
|
||||
|
||||
### 5. authTagSize = 16 bytes
|
||||
|
||||
`authTagSize` is defined as `fromIntegral C.authTagSize` (16 bytes). This is the AES-GCM authentication tag appended to the encrypted file stream. It is included in the payload size calculation (`payloadSize = fileSize' + fileSizeLen + authTagSize`), which is then passed to `prepareChunkSizes` to determine chunk allocation.
|
||||
Reference in New Issue
Block a user