diff --git a/.env.example b/.env.example index 4a261a1..a50aa13 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ FMSG_DATA_DIR=/var/lib/fmsgd/ +FMSG_DOMAIN=example.com # Production EdDSA JWT verification (uncomment to use JWKS mode). # FMSG_JWT_AUDIENCE is optional; only set it if your identity provider diff --git a/README.md b/README.md index 8269b97..f4b8f66 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ HTTP API providing user/client message handling for an fmsg host. Exposes CRUD o | Variable | Default | Description | | ------------------- | ------------------------ | ------------------------------------------------------- | | `FMSG_DATA_DIR` | *(required)* | Path where message data files are stored, e.g. `/var/lib/fmsgd/` | +| `FMSG_DOMAIN` | *(required)* | The fmsg domain this instance serves, e.g. `example.com`. Used to tell local recipients (resolved directly via fmsgid) from federated ones (left to fmsgd), independent of which participant's identity happens to be making the request. | | `FMSG_JWT_JWKS_URL` | *(prod)* | JWKS endpoint for the configured identity provider (e.g. `https://idp.example.com/.well-known/jwks.json`). When set, the API verifies EdDSA (Ed25519) JWTs. Public keys are fetched and cached, refreshed and looked up by the token's `kid` header. | | `FMSG_JWT_ISSUER` | *(prod, required with JWKS)* | Expected `iss` claim value (e.g. `https://idp.example.com/`). Tokens with a different issuer are rejected. This must exactly match the token issuer. | | `FMSG_JWT_AUDIENCE` | *(optional)* | When set, tokens must include this value in their `aud` claim. Leave unset if your identity provider does not issue an `aud` claim. | @@ -199,6 +200,7 @@ by default; override with `FMSG_API_PORT`. ```bash export FMSG_DATA_DIR=/opt/fmsg/data +export FMSG_DOMAIN=example.com export FMSG_JWT_JWKS_URL=https://idp.example.com/.well-known/jwks.json export FMSG_JWT_ISSUER=https://idp.example.com/ export FMSG_JWT_ADDRESS_CLAIM=sub @@ -227,6 +229,7 @@ proxying `https://fmsgapi.example.com/` to `http://127.0.0.1:8000/`). ```bash export FMSG_DATA_DIR=/var/lib/fmsgd/ +export FMSG_DOMAIN=example.com export FMSG_API_TOKEN_ED25519_PRIVATE_KEY=$(openssl rand -base64 32) export PGHOST=localhost export PGUSER=fmsg diff --git a/cmd/fmsg-webapi/main.go b/cmd/fmsg-webapi/main.go index acd2193..be59384 100644 --- a/cmd/fmsg-webapi/main.go +++ b/cmd/fmsg-webapi/main.go @@ -34,6 +34,9 @@ func main() { // Required configuration. dataDir := mustEnv("FMSG_DATA_DIR") + // The domain this instance serves, e.g. "example.com" — used to tell local + // recipients (resolved via fmsgid here) from federated ones (left to fmsgd). + localDomain := mustEnv("FMSG_DOMAIN") // JWT configuration. EdDSA provider JWTs and first-party Ed25519 API // tokens can be enabled independently. @@ -137,7 +140,7 @@ func main() { // Global rate limiting is handled by nftables at the host level. // Instantiate handlers. - msgHandler := handlers.NewMessageHandler(database, dataDir, maxDataSize, maxMsgSize, shortTextSize, apiStore, idURL) + msgHandler := handlers.NewMessageHandler(database, dataDir, maxDataSize, maxMsgSize, shortTextSize, apiStore, idURL, localDomain) attHandler := handlers.NewAttachmentHandler(database, dataDir, maxAttachSize, maxMsgSize) // Web Push handler: stores subscriptions and delivers VAPID pushes for diff --git a/internal/handlers/messages.go b/internal/handlers/messages.go index c83e932..ff3984a 100644 --- a/internal/handlers/messages.go +++ b/internal/handlers/messages.go @@ -34,11 +34,18 @@ type MessageHandler struct { ShortTextSize int SubAccounts *apiauth.Store IDURL string + // LocalDomain is the domain this webapi instance serves (e.g. "example.com"), + // used to decide which recipients are local vs. federated. It must NOT be + // derived from the authenticated caller's own address — a federated + // participant (e.g. @alice@other.example acting on a thread hosted here) + // has a different domain than this instance, but that has no bearing on + // which recipients are local to it. See resolveLocalDelivery. + LocalDomain string } // NewMessageHandler creates a MessageHandler. -func NewMessageHandler(database *db.DB, dataDir string, maxDataSize, maxMsgSize int64, shortTextSize int, subAccounts *apiauth.Store, idURL string) *MessageHandler { - return &MessageHandler{DB: database, DataDir: dataDir, MaxDataSize: maxDataSize, MaxMsgSize: maxMsgSize, ShortTextSize: shortTextSize, SubAccounts: subAccounts, IDURL: idURL} +func NewMessageHandler(database *db.DB, dataDir string, maxDataSize, maxMsgSize int64, shortTextSize int, subAccounts *apiauth.Store, idURL, localDomain string) *MessageHandler { + return &MessageHandler{DB: database, DataDir: dataDir, MaxDataSize: maxDataSize, MaxMsgSize: maxMsgSize, ShortTextSize: shortTextSize, SubAccounts: subAccounts, IDURL: idURL, LocalDomain: localDomain} } // visibleAddrs returns the set of fmsg addresses whose messages the caller @@ -765,10 +772,9 @@ func (h *MessageHandler) Send(c *gin.Context) { // fmsgd's outbound sender skips the local domain entirely, so local // recipients need their delivery status resolved here instead. - _, localDomain := parseAddr(identity) - h.resolveLocalDelivery(ctx, "msg_to", msgID, localDomain, existing.To) + h.resolveLocalDelivery(ctx, "msg_to", msgID, h.LocalDomain, existing.To) for _, b := range existing.AddTo { - h.resolveLocalDelivery(ctx, "msg_add_to", msgID, localDomain, b.To) + h.resolveLocalDelivery(ctx, "msg_add_to", msgID, h.LocalDomain, b.To) } c.JSON(http.StatusOK, gin.H{"id": msgID, "time": now}) @@ -955,9 +961,11 @@ func (h *MessageHandler) AddRecipients(c *gin.Context) { // fmsgd only delivers add_to batches once the parent message is sent // (mirroring its own m.time_sent IS NOT NULL gate), and skips the local // domain entirely — so resolve local recipients here for sent messages. + // Note: this must use this instance's own local domain, not the domain of + // whoever called this endpoint — the caller adding recipients may be a + // federated participant on a different domain than the recipients they add. if timeSent != nil { - _, localDomain := parseAddr(identity) - h.resolveLocalDelivery(ctx, "msg_add_to", msgID, localDomain, input.AddTo) + h.resolveLocalDelivery(ctx, "msg_add_to", msgID, h.LocalDomain, input.AddTo) } c.JSON(http.StatusOK, gin.H{"id": msgID, "added": len(input.AddTo)})