CI / Test (ubuntu-latest) (push) Waiting to run
CI / Test (windows-latest) (push) Waiting to run
CI / Storage tests on PostgreSQL (push) Waiting to run
CI / Complement (federation off) (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / Smoke test the binary (push) Waiting to run
CI / Admin panel (push) Waiting to run
CI / Web client (push) Waiting to run
CI / Desktop client (ubuntu-24.04) (push) Waiting to run
CI / Desktop client (windows-latest) (push) Waiting to run
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>
248 lines
7.8 KiB
Go
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
|
|
}
|