Skip to content

Commit 7e8a2c8

Browse files
committed
implement attachments
1 parent 41661f8 commit 7e8a2c8

4 files changed

Lines changed: 459 additions & 52 deletions

File tree

‎TODO2.md‎

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# TODO2
2+
3+
Gap analysis of the codebase against SPEC.md.
4+
5+
---
6+
7+
## P0 — Wire Format / Encoding (defs.go)
8+
9+
These are foundational: the header hash (used for challenge verification and pid
10+
references) and message hash (used for duplicate detection, challenge response,
11+
and pid) are both wrong until Encode() is complete.
12+
13+
### ~~1. Encode() must include size and attachment headers~~ DONE
14+
**File:** `defs.go` `Encode()`
15+
Spec defines "message header" as all fields through the attachment headers.
16+
Encode() currently stops after the type field — it omits:
17+
- size (uint32 LE)
18+
- attachment count (uint8) + attachment headers (flags, type, filename, size)
19+
20+
The header hash (SHA-256 of the encoded header) will be incorrect without these
21+
fields, which breaks challenge verification and pid references.
22+
23+
### 2. Encode() must omit topic when pid is set
24+
**File:** `defs.go` `Encode()`
25+
Spec: "When pid exists the entire topic field MUST NOT be included on the wire."
26+
Encode() always writes the topic. It must only write topic when pid is absent.
27+
28+
### 3. Encode() must support common-type encoding for type field
29+
**File:** `defs.go` `Encode()`
30+
When common-type flag (bit 2) is set, type on the wire is a single uint8 index,
31+
not a length-prefixed string. Encode() always writes length-prefixed.
32+
33+
### ~~4. Add Attachments field to FMsgHeader~~ DONE
34+
**File:** `defs.go` struct
35+
Add `Attachments []FMsgAttachmentHeader` to store parsed attachment headers.
36+
Required before Encode() can include attachment headers, before attachment
37+
parsing/validation/download, and before hash computation is correct.
38+
39+
### ~~5. Complete FMsgAttachmentHeader struct~~ DONE
40+
**File:** `defs.go` struct
41+
Currently only has Filename, Size, Filepath. Missing per-spec fields:
42+
- Flags (uint8) — including per-attachment common-type (bit 0) and deflate (bit 1)
43+
- Type (string) — the attachment's media type
44+
45+
### 6. Add ChallengeCompleted flag to FMsgHeader
46+
**File:** `defs.go` struct
47+
Add `ChallengeCompleted bool` to distinguish "challenge was completed and
48+
ChallengeHash is valid" from "challenge was not performed." Without this, the
49+
hash check in downloadMessage erroneously fails when the challenge was skipped.
50+
51+
### ~~7. GetMessageHash() must include attachment data~~ DONE
52+
**File:** `defs.go` `GetMessageHash()`
53+
Spec: message hash is SHA-256 of the entire message — header + data +
54+
attachment data. Currently attachment data (sequential byte sequences following
55+
the message body) is not included.
56+
57+
---
58+
59+
## P1 — Receiving: Header Exchange (host.go readHeader)
60+
61+
### 8. Generalise first-byte version/challenge detection
62+
**File:** `host.go` `readHeader()`
63+
Spec step 1.3: 1..127 = version, 129..255 = CHALLENGE (version = 256 − value),
64+
0 and 128 are undefined. Currently only v==255 is handled as a challenge. Must
65+
handle any value > 128 where (256 − value) is a supported version.
66+
67+
### 9. Send reject code 2 for unsupported version
68+
**File:** `host.go` `readHeader()`
69+
Spec 1.3.iii: Send code 2 on the connection before closing. Currently just
70+
returns an error without sending any code.
71+
72+
### 10. Validate at least one "to" recipient
73+
**File:** `host.go` `readHeader()`
74+
Spec 1.4.i.a: If to count is 0, reject code 1 (invalid). Currently no check.
75+
76+
### 11. Make topic conditional on pid absence
77+
**File:** `host.go` `readHeader()`
78+
Spec: topic field is only present when pid is NOT set. Currently topic is
79+
read unconditionally regardless of pid.
80+
81+
### 12. Handle common-type flag for type field
82+
**File:** `host.go` `readHeader()`
83+
When common-type flag (bit 2) is set, type is a single uint8 index into the
84+
Common Media Types table. If the value has no mapping → reject code 1.
85+
Currently always reads type as a length-prefixed string.
86+
87+
### 13. Parse and validate attachment headers
88+
**File:** `host.go` `readHeader()`
89+
Currently rejects any non-zero attachment count. Must:
90+
- Parse each header: flags (uint8), type, filename (uint8 + UTF-8), size (uint32).
91+
- Validate per-attachment common-type flag mapping (reject code 1 if unmapped).
92+
- Validate filenames per spec (Unicode letters/numbers, limited special chars,
93+
no consecutive dots, not at start/end, unique case-insensitive, < 256 bytes).
94+
- Include all attachment sizes in MAX_SIZE check (data size + Σ attachment sizes).
95+
- Store parsed headers on FMsgHeader.Attachments.
96+
97+
### 14. DNS-verify sender IP during header exchange
98+
**File:** `host.go` `readHeader()`
99+
Spec 1.4.ii / "Sender IP Verification": Host B MUST resolve
100+
`_fmsg.<sender-domain>` and verify the incoming connection's IP is in the
101+
authorised set. If not authorised → TERMINATE (no reject code, no challenge).
102+
Currently this check only happens inside challenge() and is skipped when
103+
challenge is not performed.
104+
105+
### 15. Perform pid verification during header exchange
106+
**File:** `host.go` `readHeader()`
107+
Spec 1.4.v.c: When pid exists and add to does not:
108+
a. pid must be verified stored per "Verifying Message Stored"; else code 6.
109+
b. Parent time must be before message time; else code 9 (time travel).
110+
Currently deferred to downloadMessage().
111+
112+
### 16. Distinguish code-200 vs code-11 parents for add-to header-only path
113+
**File:** `host.go` `readHeader()` + `store.go`
114+
Spec 1.4.v.b.a (no add-to recipients for our domain): pid MUST match a message
115+
originally accepted with code 200 (not code 11), preventing add-to chaining.
116+
Currently lookupMsgIdByHash does not distinguish how the message was accepted.
117+
118+
### 17. Verify sender participated in parent message
119+
**File:** `host.go` `readHeader()`
120+
Spec invariant: "A sender (from) MUST have been a participant in the message
121+
referenced by pid." Not currently checked.
122+
123+
---
124+
125+
## P2 — Receiving: Challenge (host.go challenge)
126+
127+
### 18. Connection 2 must target same IP as Connection 1
128+
**File:** `host.go` `challenge()`
129+
Spec 2.1: Dial conn.RemoteAddr() IP, not h.From.Domain. Dialling the domain
130+
may resolve to a different IP.
131+
132+
### 19. Make challenge mode configurable
133+
**File:** `host.go` `challenge()` / `handleConn()`
134+
Spec defines challenge modes (NEVER, ALWAYS, HAS_NOT_PARTICIPATED,
135+
DIFFERENT_DOMAIN) as Host B's implementation choice. Currently always challenges.
136+
At minimum, support skipping the challenge so the ChallengeCompleted guard
137+
(item 6) works correctly.
138+
139+
---
140+
141+
## P3 — Receiving: Download and Response (host.go downloadMessage)
142+
143+
### 20. Pre-download duplicate check via challenge hash
144+
**File:** `host.go` `downloadMessage()`
145+
Spec 3.1: BEFORE downloading data, if challenge was completed, use the message
146+
hash to check for duplicate → code 10. Currently the duplicate check is after
147+
download, wasting bandwidth.
148+
149+
### 21. Guard hash verification behind ChallengeCompleted
150+
**File:** `host.go` `downloadMessage()`
151+
Spec 3.3: Hash verification must only run when challenge was completed. Currently
152+
ChallengeHash is zero-valued when skipped, causing false mismatch on every
153+
non-challenged message.
154+
155+
### 22. Download attachment data
156+
**File:** `host.go` `downloadMessage()`
157+
Spec 3.2: Download sequential attachment byte sequences after message body,
158+
bounded by attachment header sizes. Currently only message body is downloaded.
159+
160+
### 23. Per-recipient user-duplicate check (code 103)
161+
**File:** `host.go` `downloadMessage()` / `validateMsgRecvForAddr()`
162+
Spec 3.4.i ordering: user duplicate (103) → unknown (100) → full (101) →
163+
not accepting (102) → accept (200). Currently there is no check for whether
164+
the message was already received for a specific recipient (code 103).
165+
166+
---
167+
168+
## P4 — Sending (sender.go)
169+
170+
### 24. Write actual attachment headers
171+
**File:** `sender.go` `deliverMessage()`
172+
Attachment count is hardcoded to 0. Must write attachment count + each
173+
attachment header (flags, type, filename, size) from FMsgHeader.Attachments.
174+
175+
### 25. Send attachment data after message body
176+
**File:** `sender.go` `deliverMessage()`
177+
Spec: sequential attachment byte sequences following data, bounded by header
178+
sizes. Currently not sent.
179+
180+
### 26. Implement exponential back-off for retries
181+
**File:** `sender.go` `findPendingTargets()`
182+
Spec says "SHOULD apply a back-off strategy." Currently uses a fixed
183+
RetryInterval.
184+
185+
### 27. Add code 101 (user full) to retryable set
186+
**File:** `sender.go` `findPendingTargets()`
187+
Per-user code 101 is analogous to global code 5 and is likely transient.
188+
Spec says MAY warrant retry.
189+
190+
---
191+
192+
## P5 — Storage (store.go / dd.sql)
193+
194+
### 28. Store and load attachment metadata
195+
**Files:** `store.go` `storeMsgDetail()` / `loadMsg()`, `dd.sql` msg_attachment
196+
- dd.sql: msg_attachment is missing `flags` (uint8) and `type` (varchar)
197+
columns needed for wire-format reconstruction and hash computation.
198+
- storeMsgDetail: insert attachment headers into msg_attachment.
199+
- loadMsg: load attachment rows into FMsgHeader.Attachments.
200+
201+
### 29. Distinguish acceptance mode in stored messages
202+
**File:** `store.go` / `dd.sql`
203+
For item 16 (preventing add-to chaining), need a way to distinguish messages
204+
accepted via code 200 (full message) from code 11 (header-only add-to
205+
notification). Options: boolean column, separate lookup, or filepath="" check.
206+
207+
---
208+
209+
## P6 — Address Validation (host.go)
210+
211+
### 30. Support Unicode in address recipient part
212+
**File:** `host.go` `isValidUser()`
213+
Spec says recipient may contain Unicode letters/numbers (`\p{L}`, `\p{N}`).
214+
Currently only ASCII a-z, A-Z, 0-9 are accepted. Must use Unicode-aware
215+
checks (e.g. `unicode.IsLetter`, `unicode.IsNumber`).
216+
217+
### 31. Enforce dot placement rules in address recipient part
218+
**File:** `host.go` `isValidUser()`
219+
Spec says dot `.` must not be consecutive and not at start/end. Currently dots
220+
are allowed anywhere with no positional checks.
221+
222+
---
223+
224+
## P7 — DNS / DNSSEC (dns.go)
225+
226+
### 32. Perform DNSSEC validation
227+
**Files:** `dns.go` `lookupAuthorisedIPs()`, `sender.go`
228+
Spec: DNSSEC validation SHOULD be performed. If validation fails → connection
229+
MUST terminate (no retry). Currently not performed or reported.
230+
231+
---
232+
233+
## P8 — Handling a Challenge (host.go)
234+
235+
### 33. Incoming challenge must send reject code 2 for unsupported version
236+
**File:** `host.go` `readHeader()` / `handleChallenge()`
237+
Spec "Handling a Challenge" step 1: if the first byte is not a supported
238+
version and not a valid challenge, send reject code 2 (unsupported version)
239+
and close. Currently handleChallenge is only entered for v==255 and other
240+
unsupported values return an error without sending a code.

‎src/defs.go‎

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type FMsgAddress struct {
1717
}
1818

1919
type FMsgAttachmentHeader struct {
20+
Flags uint8
21+
Type string
2022
Filename string
2123
Size uint32
2224

@@ -35,11 +37,9 @@ type FMsgHeader struct {
3537
Type string
3638

3739
// Size in bytes of entire message
38-
Size uint32
39-
// TODO [Spec]: Add Attachments []FMsgAttachmentHeader field to store parsed
40-
// attachment headers (flags, type, filename, size) from the wire format.
40+
Size uint32
41+
Attachments []FMsgAttachmentHeader
4142

42-
// Hash up to and including Type
4343
HeaderHash []byte
4444
// Hash of message from challenge response
4545
ChallengeHash [32]byte
@@ -58,16 +58,9 @@ func (addr *FMsgAddress) ToString() string {
5858
return fmt.Sprintf("@%s@%s", addr.User, addr.Domain)
5959
}
6060

61-
// Encode the header up to and including type field to a []byte. This function will panic on error
62-
// instead of returning one.
63-
// TODO [Spec]: The spec defines "message header" as all fields up to and
64-
// including the attachment headers field. This Encode() is missing:
65-
// - The "size" field (uint32).
66-
// - The "attachment headers" field (uint8 count + list of attachment headers).
67-
//
68-
// The header hash (SHA-256 of the encoded header) will be incorrect without
69-
// these fields, breaking challenge verification and pid references.
70-
// Additionally, the topic field should only be encoded when pid is NOT set.
61+
// Encode the message header to wire format as a []byte. This includes all
62+
// fields up to and including the attachment headers per spec. This function
63+
// will panic on error instead of returning one.
7164
func (h *FMsgHeader) Encode() []byte {
7265
var b bytes.Buffer
7366
b.WriteByte(h.Version)
@@ -95,10 +88,29 @@ func (h *FMsgHeader) Encode() []byte {
9588
if err := binary.Write(&b, binary.LittleEndian, h.Timestamp); err != nil {
9689
panic(err)
9790
}
98-
b.WriteByte(byte(len(h.Topic)))
99-
b.WriteString(h.Topic)
91+
// topic is only present when pid is NOT set
92+
if h.Flags&FlagHasPid == 0 {
93+
b.WriteByte(byte(len(h.Topic)))
94+
b.WriteString(h.Topic)
95+
}
10096
b.WriteByte(byte(len(h.Type)))
10197
b.WriteString(h.Type)
98+
// size (uint32 LE)
99+
if err := binary.Write(&b, binary.LittleEndian, h.Size); err != nil {
100+
panic(err)
101+
}
102+
// attachment headers
103+
b.WriteByte(byte(len(h.Attachments)))
104+
for _, att := range h.Attachments {
105+
b.WriteByte(att.Flags)
106+
b.WriteByte(byte(len(att.Type)))
107+
b.WriteString(att.Type)
108+
b.WriteByte(byte(len(att.Filename)))
109+
b.WriteString(att.Filename)
110+
if err := binary.Write(&b, binary.LittleEndian, att.Size); err != nil {
111+
panic(err)
112+
}
113+
}
102114
return b.Bytes()
103115
}
104116

@@ -153,17 +165,23 @@ func (h *FMsgHeader) GetMessageHash() ([]byte, error) {
153165
return nil, err
154166
}
155167

156-
// TODO [Spec]: Encode() is missing size, attachment count, and
157-
// attachment headers. Once Encode() includes the full message header
158-
// per spec, the size and attachment header bytes will automatically be
159-
// included in this hash.
160-
161168
if _, err := io.Copy(hash, f); err != nil {
162169
return nil, err
163170
}
164171

165-
// TODO: include attachment data (sequential byte sequences following
166-
// the message body, bounded by attachment header sizes) in the hash.
172+
// include attachment data (sequential byte sequences following
173+
// the message body, bounded by attachment header sizes)
174+
for _, att := range h.Attachments {
175+
af, err := os.Open(att.Filepath)
176+
if err != nil {
177+
return nil, fmt.Errorf("open attachment %s: %w", att.Filename, err)
178+
}
179+
if _, err := io.CopyN(hash, af, int64(att.Size)); err != nil {
180+
af.Close()
181+
return nil, fmt.Errorf("read attachment %s: %w", att.Filename, err)
182+
}
183+
af.Close()
184+
}
167185

168186
h.messageHash = hash.Sum(nil)
169187
}

0 commit comments

Comments
 (0)