Address code review findings across API, web and deployment
release gate / check (push) Failing after 6m43s
release gate / check (push) Failing after 6m43s
High severity: - wire TRUSTED_PROXY through Compose/deploy/docs and warn at web startup - bucket public rate limits on effective client IP, add per-bookmark budget, and forward the signed browser identity from the share page - disable first-run bootstrap in server deployments and document seeding - serialize last-admin demotion in one advisory-locked transaction Medium/low: - cap and batch share recipient inserts - add a shared outbound budget for preview/CardDAV/IMAP/signal work - pin CardDAV dials to 443 and bound IMAP logout - fix rightward kanban column drags and normalize cards on column delete - classify backend outages in auth guards, add request timeouts and an error boundary, remove the dead login route, surface throttling - enforce pinned/unpinned page reorder grouping server-side - avoid preview work for stale bookmark versions; drop duplicate dialog fetch - sequence-guard collection/tag/calendar/quick-find loads - validate property config and fix MULTI_SELECT/DATE filter inputs - document index maintenance; apply same-origin check to sign-out - trim signup names, unique shared dates and kanban keys (migration 010), reset reference cache on identity changes, hide OIDC issuer, return 413 for oversized direct bodies, escape Markdown link destinations - remove dead template files and duplicate test helper Verified with go build/vet, full PostgreSQL race suite, db convergence, tsc, both ESLint configs and a Next production build.
This commit is contained in:
@@ -9,8 +9,10 @@ import (
|
||||
crypt "github.com/andrew/go-computer/internal/crypto"
|
||||
"github.com/andrew/go-computer/internal/db"
|
||||
"github.com/andrew/go-computer/internal/netguard"
|
||||
"github.com/andrew/go-computer/internal/outbound"
|
||||
"github.com/andrew/go-computer/internal/signals"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -18,12 +20,24 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// carddavDial pins production CardDAV traffic to 443 (review M-3), matching
|
||||
// previews (80/443) and IMAP (143/993). Without it a user could make the
|
||||
// server TLS-handshake and send Basic-auth credentials to any public port.
|
||||
// The fixture/LAN escape hatch keeps loopback fakes and deliberate private
|
||||
// deployments working.
|
||||
func carddavDial(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if netguard.AllowPrivate() {
|
||||
return netguard.DialContext(ctx, network, address)
|
||||
}
|
||||
return netguard.DialContextPorts(ctx, network, address, 443)
|
||||
}
|
||||
|
||||
// carddavClient dials through netguard so a user-supplied Nextcloud URL can
|
||||
// never point the server at internal infrastructure (same guard as URL
|
||||
// previews). The transport is shared to avoid leaking idle sockets per sync.
|
||||
var carddavClient = &http.Client{
|
||||
Timeout: 20 * time.Second,
|
||||
Transport: &http.Transport{DialContext: netguard.DialContext},
|
||||
Transport: &http.Transport{DialContext: carddavDial},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
@@ -65,7 +79,7 @@ func HandleSharedDatesCreate(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, 0, nil, bad("Date required"))
|
||||
return
|
||||
}
|
||||
list, e := ids(b, "recipientIds")
|
||||
list, e := cappedIDs(b, "recipientIds", maxSharedDateRecipients)
|
||||
if e != nil || len(list) == 0 {
|
||||
respond(w, 0, nil, bad("Recipients required"))
|
||||
return
|
||||
@@ -81,7 +95,11 @@ func HandleSharedDatesCreate(w http.ResponseWriter, r *http.Request) {
|
||||
out := []object{}
|
||||
e = db.Transaction(r.Context(), database(r), func(tx *sql.Tx) error {
|
||||
for _, id := range list {
|
||||
v, e := db.Insert(r.Context(), tx, "SharedDate", object{"id": db.NewID(), "date": dt, "creatorId": user(r), "recipientId": id, "note": b["note"]})
|
||||
// Review L-3: re-sharing the same creator/recipient/date updates
|
||||
// the note instead of inserting a duplicate calendar row.
|
||||
v, e := db.One(r.Context(), tx, `INSERT INTO "SharedDate"(id,date,note,"creatorId","recipientId") VALUES($1,$2,$3,$4,$5)
|
||||
ON CONFLICT ("creatorId","recipientId",date) DO UPDATE SET note=EXCLUDED.note
|
||||
RETURNING to_jsonb("SharedDate")`, db.NewID(), dt, b["note"], user(r), id)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -149,6 +167,13 @@ func HandleContactsSync(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, 0, nil, bad("Nextcloud URL must use https"))
|
||||
return
|
||||
}
|
||||
// Review M-3: the transport pins production dials to 443; reject an
|
||||
// explicit non-standard port up front for a clear error instead of a
|
||||
// generic connection failure.
|
||||
if !netguard.AllowPrivate() && base.Port() != "" && base.Port() != "443" {
|
||||
respond(w, 0, nil, bad("Nextcloud URL must use port 443"))
|
||||
return
|
||||
}
|
||||
path := str(b, "cardDavPath")
|
||||
if path == "" {
|
||||
path = "/remote.php/dav/addressbooks/users/" + url.PathEscape(str(b, "username")) + "/contacts/"
|
||||
@@ -158,6 +183,15 @@ func HandleContactsSync(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, 0, nil, e)
|
||||
return
|
||||
}
|
||||
// Review M-2: CardDAV sync draws from the shared outbound budget with
|
||||
// IMAP/preview/signal checks, so one user cannot open unbounded
|
||||
// long-lived connections or starve egress.
|
||||
release, retry, ok := outbound.Reserve("carddav:"+user(r), 10, time.Minute)
|
||||
if !ok {
|
||||
outboundBusy(w, retry)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
payload := `<?xml version="1.0"?><card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"><d:prop><d:getetag/><card:address-data/></d:prop></card:addressbook-query>`
|
||||
req, e := http.NewRequestWithContext(r.Context(), "REPORT", target.String(), strings.NewReader(payload))
|
||||
if e != nil {
|
||||
@@ -493,11 +527,20 @@ func HandleImapTest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
m.Password = saved.Password
|
||||
}
|
||||
release, retry, ok := outbound.Reserve("imap:"+user(r), 10, time.Minute)
|
||||
if !ok {
|
||||
outboundBusy(w, retry)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
c, e := signals.Connect(r.Context(), m)
|
||||
if e != nil {
|
||||
respond(w, 0, nil, bad("Mailbox connection or login failed"))
|
||||
return
|
||||
}
|
||||
// Review M-2: a hostile server must not hold the handler open during the
|
||||
// goodbye exchange after the test has already succeeded.
|
||||
c.Timeout = 5 * time.Second
|
||||
c.Logout()
|
||||
respond(w, 200, OK{true}, nil)
|
||||
}
|
||||
@@ -523,6 +566,12 @@ func HandleImapCheck(w http.ResponseWriter, r *http.Request) {
|
||||
respond(w, 0, nil, bad("Configure IMAP first"))
|
||||
return
|
||||
}
|
||||
release, retry, ok := outbound.Reserve("imap:"+user(r), 10, time.Minute)
|
||||
if !ok {
|
||||
outboundBusy(w, retry)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
|
||||
defer cancel()
|
||||
c, e := signals.Connect(ctx, m)
|
||||
@@ -532,7 +581,9 @@ func HandleImapCheck(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
stop := context.AfterFunc(ctx, func() { c.Terminate() })
|
||||
defer stop()
|
||||
defer c.Logout()
|
||||
// Keep the goodbye exchange bounded too (review M-2); searches above
|
||||
// stay under the caller's 45-second context.
|
||||
defer func() { c.Timeout = 5 * time.Second; c.Logout() }()
|
||||
searchFailed := false
|
||||
for _, b := range watched {
|
||||
events, e := signals.SearchConn(c, m, "", str(b, "imapQuery"))
|
||||
|
||||
Reference in New Issue
Block a user