Files
Warp/server/internal/storage/pocketbase/remotekeys.go
T
tajniak81andClaude Opus 5.5 a47e15cc1c
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
CI / Admin panel (push) Canceled after 0s
CI / Web client (push) Canceled after 0s
CI / Desktop client (ubuntu-24.04) (push) Canceled after 0s
CI / Desktop client (windows-latest) (push) Canceled after 0s
PocketBase as a standalone database server
The pocketbase driver no longer embeds PocketBase: Warp keeps its data as
PocketBase collections on a server of its own and reaches it only through
the REST API, the way the postgres driver reaches PostgreSQL.
database.connection_string is the server's URL, and Warp signs in as the
superuser in WARP_PB_SUPERUSER_EMAIL / WARP_PB_SUPERUSER_PASSWORD (or a
WARP_PB_SUPERUSER_FILE of the two).

Every SQL table is a superuser-only collection of the same name and
columns, created by warp migrate. Reads go through PocketBase's SQL
endpoint with the SQL drivers' queries, via a read-only database/sql
driver that inlines arguments, moves long ones into warp_params and pages
past 1000 rows. Writes are record operations in batch requests under one
write lock in the Warp process. Media is a protected file field in
warp_media, local or in the S3 bucket Warp writes into PocketBase's
settings. Search matches word prefixes with LIKE. The storage tests run
against an in-process PocketBase and pass as on SQLite.

Docker: the PocketBase stacks get a pocketbase service running
tajniak81/pocketbase-docker-root (WARP_PB_IMAGE), with a pb-data volume
and its admin UI on loopback; the AIO images run that PocketBase binary
as a process beside the homeserver, making up a superuser when none is
set. The .env examples, READMEs, docs and warp.yaml.example follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:31:08 +01:00

248 lines
7.8 KiB
Go

package pocketbase
import (
"context"
"database/sql"
"errors"
"fmt"
"slices"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
// The cache of other servers' users' keys, as the SQL store keeps it.
var remoteKeyTables = []string{
"keyserver_remote_users", "keyserver_remote_device_keys", "keyserver_remote_cross_signing_keys",
}
// remoteMarker reads a cached user's marker row.
func remoteMarker(row rowQuerier, userID string) (streamID int64, stale, ok bool, err error) {
err = row(`SELECT stream_id, stale FROM keyserver_remote_users WHERE user_id = ?`, userID).Scan(&streamID, &stale)
switch {
case errors.Is(err, sql.ErrNoRows):
return 0, false, false, nil
case err != nil:
return 0, false, false, fmt.Errorf("read the cached keys of %q: %w", userID, err)
}
return streamID, stale, true, nil
}
// RemoteKeys implements storage.KeyStore. The marker and the keys are read
// under one read lock, so that a copy halfway through its replacement is
// never served.
func (s *Keys) RemoteKeys(ctx context.Context, userID string) (storage.RemoteKeys, bool, error) {
out := storage.RemoteKeys{UserID: userID, CrossSigning: map[string][]byte{}}
var found bool
err := s.read(ctx, func(ctx context.Context) error {
var err error
out.StreamID, out.Stale, found, err = remoteMarker(s.poolRow(ctx), userID)
if err != nil || !found {
return err
}
if out.Devices, err = s.remoteDevices(ctx, userID); err != nil {
return err
}
out.CrossSigning, err = s.remoteCrossSigning(ctx, userID)
return err
})
if err != nil || !found {
return storage.RemoteKeys{}, false, err
}
return out, true, nil
}
func (s *Keys) remoteDevices(ctx context.Context, userID string) ([]storage.RemoteDevice, error) {
rows, err := s.db.QueryContext(ctx, `SELECT device_id, display_name, key_json FROM keyserver_remote_device_keys
WHERE user_id = ? ORDER BY device_id`, userID)
if err != nil {
return nil, fmt.Errorf("read the cached devices of %q: %w", userID, err)
}
defer func() { _ = rows.Close() }()
var out []storage.RemoteDevice
for rows.Next() {
var d storage.RemoteDevice
var keyJSON string
if err := rows.Scan(&d.DeviceID, &d.DisplayName, &keyJSON); err != nil {
return nil, fmt.Errorf("read the cached devices of %q: %w", userID, err)
}
d.JSON = []byte(keyJSON)
out = append(out, d)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("read the cached devices of %q: %w", userID, err)
}
return out, nil
}
func (s *Keys) remoteCrossSigning(ctx context.Context, userID string) (map[string][]byte, error) {
rows, err := s.db.QueryContext(ctx, `SELECT purpose, key_json FROM keyserver_remote_cross_signing_keys
WHERE user_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("read the cached cross-signing keys of %q: %w", userID, err)
}
defer func() { _ = rows.Close() }()
out := map[string][]byte{}
for rows.Next() {
var purpose, keyJSON string
if err := rows.Scan(&purpose, &keyJSON); err != nil {
return nil, fmt.Errorf("read the cached cross-signing keys of %q: %w", userID, err)
}
out[purpose] = []byte(keyJSON)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("read the cached cross-signing keys of %q: %w", userID, err)
}
return out, nil
}
// SetRemoteKeys implements storage.KeyStore. A snapshot older than what is
// held is not stored: it was fetched while an update was on its way in.
func (s *Keys) SetRemoteKeys(ctx context.Context, keys storage.RemoteKeys) (bool, error) {
stored := false
err := s.write(ctx, func(t *tx) error {
held, _, ok, err := remoteMarker(t.queryRow, keys.UserID)
if err != nil {
return err
}
if ok && held > keys.StreamID {
return nil
}
if err := deleteRemote(t, keys.UserID); err != nil {
return err
}
t.put("keyserver_remote_users", rec{"user_id": keys.UserID, "stream_id": keys.StreamID, "stale": false})
for _, d := range keys.Devices {
t.put("keyserver_remote_device_keys", rec{
"user_id": keys.UserID, "device_id": d.DeviceID,
"display_name": d.DisplayName, "key_json": string(d.JSON),
})
}
putRemoteCrossSigning(t, keys.UserID, keys.CrossSigning)
stored = true
return nil
})
return stored, err
}
// UpdateRemoteDevice implements storage.KeyStore.
func (s *Keys) UpdateRemoteDevice(ctx context.Context, u storage.RemoteDeviceUpdate) (storage.RemoteUpdateResult, error) {
var result storage.RemoteUpdateResult
err := s.write(ctx, func(t *tx) error {
held, stale, ok, err := remoteMarker(t.queryRow, u.UserID)
if err != nil {
return err
}
// A stale copy keeps the highest position it has heard of, so that
// a snapshot fetched before this update cannot be stored over it.
markStale := func(r storage.RemoteUpdateResult) error {
result = r
t.put("keyserver_remote_users", rec{"user_id": u.UserID, "stream_id": max(held, u.StreamID), "stale": true})
return nil
}
switch {
case !ok:
return markStale(storage.RemoteUpdateNotCached)
case stale:
return markStale(storage.RemoteUpdateStale)
case u.StreamID <= held:
result = storage.RemoteUpdateDuplicate
return nil
case !followsOn(held, u.PrevIDs):
return markStale(storage.RemoteUpdateStale)
case !u.Deleted && u.JSON == nil:
return markStale(storage.RemoteUpdateStale)
}
if u.Deleted {
if _, err := t.delKey("keyserver_remote_device_keys", u.UserID, u.DeviceID); err != nil {
return fmt.Errorf("apply an update to the cached keys of %s/%s: %w", u.UserID, u.DeviceID, err)
}
} else {
t.put("keyserver_remote_device_keys", rec{
"user_id": u.UserID, "device_id": u.DeviceID,
"display_name": u.DisplayName, "key_json": string(u.JSON),
})
}
t.put("keyserver_remote_users", rec{"user_id": u.UserID, "stream_id": u.StreamID, "stale": false})
result = storage.RemoteUpdateApplied
return nil
})
return result, err
}
// followsOn reports whether an update with the given prev_id comes right
// after the position held.
func followsOn(held int64, prevIDs []int64) bool {
if len(prevIDs) == 0 {
return held == 0
}
return slices.Contains(prevIDs, held)
}
// SetRemoteCrossSigningKeys implements storage.KeyStore.
func (s *Keys) SetRemoteCrossSigningKeys(ctx context.Context, userID string, keys map[string][]byte) (bool, error) {
stored := false
err := s.write(ctx, func(t *tx) error {
_, stale, ok, err := remoteMarker(t.queryRow, userID)
if err != nil || !ok || stale {
return err
}
if _, err := t.delWhere("keyserver_remote_cross_signing_keys", `user_id = ?`, userID); err != nil {
return fmt.Errorf("replace the cached cross-signing keys of %q: %w", userID, err)
}
putRemoteCrossSigning(t, userID, keys)
stored = true
return nil
})
return stored, err
}
func putRemoteCrossSigning(t *tx, userID string, keys map[string][]byte) {
for _, purpose := range sortedKeys(keys) {
if keys[purpose] == nil {
continue
}
t.put("keyserver_remote_cross_signing_keys", rec{"user_id": userID, "purpose": purpose, "key_json": string(keys[purpose])})
}
}
// RemoteKeyUsers implements storage.KeyStore.
func (s *Keys) RemoteKeyUsers(ctx context.Context) ([]string, error) {
out, err := s.queryStrings(ctx, `SELECT user_id FROM keyserver_remote_users ORDER BY user_id`)
if err != nil {
return nil, fmt.Errorf("read the users with cached keys: %w", err)
}
return out, nil
}
// DeleteRemoteKeys implements storage.KeyStore.
func (s *Keys) DeleteRemoteKeys(ctx context.Context, userIDs []string) error {
if len(userIDs) == 0 {
return nil
}
return s.write(ctx, func(t *tx) error {
for _, userID := range dedupe(userIDs) {
if err := deleteRemote(t, userID); err != nil {
return err
}
}
return nil
})
}
// deleteRemote drops every row cached for a user.
func deleteRemote(t *tx, userID string) error {
for _, table := range remoteKeyTables {
if _, err := t.delWhere(table, `user_id = ?`, userID); err != nil {
return fmt.Errorf("delete from %s for %q: %w", table, userID, err)
}
}
return nil
}