121 lines
4.3 KiB
Go
121 lines
4.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/url"
|
|
"testing"
|
|
)
|
|
|
|
func TestMailboxRejectsUnusualPort(t *testing.T) {
|
|
t.Setenv("INTEGRATIONS_ALLOW_PRIVATE", "")
|
|
t.Setenv("INTEGRATIONS_ALLOW_INSECURE_IMAP", "")
|
|
if _, e := mailboxData(object{"host": "mail.example.com", "username": "u", "password": "p", "port": float64(22), "tls": true}); e == nil {
|
|
t.Fatal("mailbox accepted a non-IMAP port")
|
|
}
|
|
if _, e := mailboxData(object{"host": "mail.example.com", "username": "u", "password": "p", "port": float64(993), "tls": true}); e != nil {
|
|
t.Fatalf("normal IMAP port rejected: %v", e)
|
|
}
|
|
}
|
|
|
|
// TestResolveCardDAVTarget pins review CR-01: the COMPLETE resolved
|
|
// destination is revalidated, so a same-host absolute http:// path cannot
|
|
// downgrade an https base and carry Basic credentials over plaintext.
|
|
func TestResolveCardDAVTarget(t *testing.T) {
|
|
base, e := url.Parse("https://cloud.example.com/remote.php/dav")
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
accepted := []string{
|
|
"/remote.php/dav/addressbooks/users/alice/contacts/",
|
|
"addressbooks/users/alice/contacts/",
|
|
"https://cloud.example.com/remote.php/dav/addressbooks/users/alice/contacts/",
|
|
}
|
|
for _, path := range accepted {
|
|
target, e := resolveCardDAVTarget(base, path)
|
|
if e != nil {
|
|
t.Fatalf("resolveCardDAVTarget(%q) rejected: %v", path, e)
|
|
}
|
|
if target.Scheme != "https" || target.Host != "cloud.example.com" {
|
|
t.Fatalf("resolveCardDAVTarget(%q) resolved to %s", path, target)
|
|
}
|
|
}
|
|
rejected := []string{
|
|
// Same host, downgraded scheme — the CR-01 credential-leak vector.
|
|
"http://cloud.example.com/remote.php/dav/addressbooks/users/alice/contacts/",
|
|
// Cross-host absolute URLs.
|
|
"https://evil.example.com/remote.php/dav/",
|
|
"http://evil.example.com/remote.php/dav/",
|
|
// Scheme-relative and userinfo tricks.
|
|
"//evil.example.com/dav/",
|
|
"https://alice:pw@cloud.example.com/dav/",
|
|
}
|
|
for _, path := range rejected {
|
|
if target, e := resolveCardDAVTarget(base, path); e == nil {
|
|
t.Fatalf("resolveCardDAVTarget(%q) accepted, resolved to %s", path, target)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestParseVCardSubset pins the documented parser subset (review CR-08):
|
|
// repeated properties are preserved, PREF selection is deterministic,
|
|
// groups/params/escapes/folding are handled, and base64-encoded properties
|
|
// are dropped instead of silently misparsed.
|
|
func TestParseVCardSubset(t *testing.T) {
|
|
card := "BEGIN:VCARD\r\n" +
|
|
"VERSION:3.0\r\n" +
|
|
"UID:uid-1\r\n" +
|
|
"item1.FN:Preferred Name\r\n" +
|
|
"item1.X-ABLabel:Main\r\n" +
|
|
"EMAIL;TYPE=WORK:work@computer.test\r\n" +
|
|
"EMAIL;TYPE=PREF:pref@computer.test\r\n" +
|
|
"EMAIL:plain@computer.test\r\n" +
|
|
"TEL;TYPE=HOME,VOICE:+1 555 0100\r\n" +
|
|
"NOTE:folded line\r\n" +
|
|
" continuation\r\n" +
|
|
"X-ESCAPED:a\\,b\\;c\\\\d\\ne\r\n" +
|
|
"PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQSkZJRg==\r\n" +
|
|
"END:VCARD"
|
|
v := parseVCard(card)
|
|
|
|
if got := vCardPick(v["UID"]); got != "uid-1" {
|
|
t.Fatalf("UID = %q", got)
|
|
}
|
|
// Group prefix stripped (item1.FN -> FN).
|
|
if got := vCardPick(v["FN"]); got != "Preferred Name" {
|
|
t.Fatalf("FN = %q", got)
|
|
}
|
|
// All three EMAIL occurrences survive; TYPE=PREF wins deterministically.
|
|
if len(v["EMAIL"]) != 3 {
|
|
t.Fatalf("repeated EMAILs not preserved: %d", len(v["EMAIL"]))
|
|
}
|
|
if got := vCardPick(v["EMAIL"]); got != "pref@computer.test" {
|
|
t.Fatalf("PREF selection = %q", got)
|
|
}
|
|
// vCard 4.0 numeric PREF ranks below any TYPE=PREF-free occurrence.
|
|
num := parseVCard("EMAIL:a@x.test\r\nEMAIL;PREF=2:b@x.test\r\nEMAIL;PREF=1:c@x.test\r\n")
|
|
if got := vCardPick(num["EMAIL"]); got != "c@x.test" {
|
|
t.Fatalf("numeric PREF selection = %q", got)
|
|
}
|
|
// Multi-value parameters (TYPE=HOME,VOICE) are split.
|
|
tel := v["TEL"]
|
|
if len(tel) != 1 || tel[0].params["TYPE"][0] != "HOME" || tel[0].params["TYPE"][1] != "VOICE" {
|
|
t.Fatalf("TEL params = %+v", tel)
|
|
}
|
|
// CRLF-space folding unfolds into one value: the first space is the fold
|
|
// marker, the second is content.
|
|
if got := vCardPick(v["NOTE"]); got != "folded line continuation" {
|
|
t.Fatalf("NOTE unfolding = %q", got)
|
|
}
|
|
// Escapes decode.
|
|
if got := vCardPick(v["X-ESCAPED"]); got != "a,b;c\\d\ne" {
|
|
t.Fatalf("escapes = %q", got)
|
|
}
|
|
// Base64-encoded properties are dropped, not stored as garbage.
|
|
if _, ok := v["PHOTO"]; ok {
|
|
t.Fatal("base64-encoded PHOTO was parsed instead of dropped")
|
|
}
|
|
// Missing properties pick to "".
|
|
if got := vCardPick(v["ORG"]); got != "" {
|
|
t.Fatalf("missing property = %q", got)
|
|
}
|
|
}
|