Phase 6: media
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 / 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
Implements PLAN.md Phase 6: the media repository. Clients upload files, reserve an mxc:// URI and upload to it later, download and ask for thumbnails, all through the authenticated media endpoints of Matrix 1.11; other servers download this server's media through the federation media endpoints, and media uploaded elsewhere is fetched over federation on first use and kept. internal/mediaapi is the component. Content goes into a BlobStore named by its SHA-256, so the same file uploaded twice is stored once and the hash doubles as the ETag; the metadata that maps an mxc:// URI onto a blob is in media_repository and media_thumbnails. The local-filesystem store writes to a temporary file and renames it into place once hashed, so nothing ever refers to half a file. A storage driver can bring its own store through storage.BlobProvider, which is where PocketBase's filesystem abstraction (local or S3) plugs in with Phase 10. Reserved uploads belong to the user who made them, take one upload, lapse after a day, and are capped per user; a download of one waits for its content, woken in memory the moment it arrives, for as long as timeout_ms allows. Thumbnails come from a fixed set of configured sizes, so a thousand requested sizes cannot mean a thousand resizes. They are made on first request and kept, never enlarge, honour a JPEG's EXIF orientation, and point at the original when it already fits. The pixel count is checked from the header before anything is decoded, at most four resizes run at once, and concurrent requests for the same thumbnail share one. Remote media is fetched through the signed client: the multipart/mixed answer of Matrix 1.11, with a fallback to the legacy download endpoint for servers that answer M_UNRECOGNIZED, and Location redirects followed by a dialer that refuses loopback, private and link-local addresses. Concurrent requests share one fetch, detached from the request that started it. With federation off nothing is fetched, and what was cached before is still served. Downloads carry a sandboxing CSP, nosniff, inline Content-Disposition only for the types the spec lists as safe, and private immutable caching. The unauthenticated legacy download endpoints are off by default behind media.legacy_unauthenticated_downloads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6aaf554336
commit
92836f8dcd
40 files changed
+4729
-63
No files matched your search
@@ -0,0 +1,34 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/dariuszjaniak/warp/server/internal/config"
|
||||
"github.com/dariuszjaniak/warp/server/internal/mediaapi"
|
||||
"github.com/dariuszjaniak/warp/server/internal/storage"
|
||||
"github.com/dariuszjaniak/warp/server/internal/storage/filestore"
|
||||
)
|
||||
|
||||
// newMedia builds the media repository. Its content goes to the storage
|
||||
// driver's own file storage if it has one, and under media.base_path if
|
||||
// not.
|
||||
func newMedia(cfg *config.Config, db storage.Database, log *slog.Logger) (*mediaapi.API, error) {
|
||||
var blobs storage.BlobStore
|
||||
if provider, ok := db.(storage.BlobProvider); ok {
|
||||
blobs = provider.Blobs()
|
||||
} else {
|
||||
store, err := filestore.New(cfg.Media.BasePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobs = store
|
||||
log.Debug("media is stored on the local filesystem", "path", store.Root())
|
||||
}
|
||||
return mediaapi.New(mediaapi.Options{
|
||||
ServerName: cfg.Global.ServerName,
|
||||
Store: db.Media(),
|
||||
Blobs: blobs,
|
||||
Config: cfg.Media,
|
||||
Logger: log,
|
||||
})
|
||||
}
|
||||
@@ -103,6 +103,7 @@ func newFederation(t *testing.T, names ...string) map[string]*fedNode {
|
||||
cfg.Registration.Enabled = true
|
||||
cfg.ClientAPI.RateLimit.Enabled = false
|
||||
cfg.Federation.Enabled = true
|
||||
cfg.Media.BasePath = filepath.Join(t.TempDir(), name+"-media")
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("config: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
package setup_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dariuszjaniak/warp/server/internal/config"
|
||||
"github.com/dariuszjaniak/warp/server/internal/setup"
|
||||
)
|
||||
|
||||
// newMediaServer is a server with its media in a temporary directory.
|
||||
func newMediaServer(t *testing.T, tweak func(*config.Config)) *setup.Server {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
return newTestServer(t, func(cfg *config.Config) {
|
||||
cfg.Registration.Enabled = true
|
||||
cfg.ClientAPI.RateLimit.Enabled = false
|
||||
cfg.Media.BasePath = filepath.Join(dir, "media")
|
||||
if tweak != nil {
|
||||
tweak(cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// send is a request with a raw body.
|
||||
func send(t *testing.T, srv *setup.Server, method, path, contentType string, body []byte, token string, header http.Header) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(method, path, bytes.NewReader(body))
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
for k, v := range header {
|
||||
req.Header[k] = v
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
// uploadThrough uploads content and returns the server name and media ID
|
||||
// of its mxc:// URI.
|
||||
func uploadThrough(t *testing.T, srv *setup.Server, token, contentType, fileName string, body []byte) (string, string) {
|
||||
t.Helper()
|
||||
path := "/_matrix/media/v3/upload"
|
||||
if fileName != "" {
|
||||
path += "?filename=" + fileName
|
||||
}
|
||||
rec := send(t, srv, http.MethodPost, path, contentType, body, token, nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("upload: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
return splitMXC(t, decode[map[string]string](t, rec)["content_uri"])
|
||||
}
|
||||
|
||||
func splitMXC(t *testing.T, mxc string) (string, string) {
|
||||
t.Helper()
|
||||
rest, ok := strings.CutPrefix(mxc, "mxc://")
|
||||
server, id, found := strings.Cut(rest, "/")
|
||||
if !ok || !found {
|
||||
t.Fatalf("bad mxc URI %q", mxc)
|
||||
}
|
||||
return server, id
|
||||
}
|
||||
|
||||
func pngOf(t *testing.T, w, h int) []byte {
|
||||
t.Helper()
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := range h {
|
||||
for x := range w {
|
||||
img.Set(x, y, color.RGBA{uint8(x), uint8(y), 200, 255})
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestMediaIsWiredUp(t *testing.T) {
|
||||
srv := newMediaServer(t, nil)
|
||||
token := registerThrough(t, srv, "alice")
|
||||
|
||||
rec := getWithToken(t, srv, "/_matrix/client/v1/media/config", token)
|
||||
if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"m.upload.size":52428800`) {
|
||||
t.Fatalf("media config: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
server, id := uploadThrough(t, srv, token, "text/plain", "hello.txt", []byte("hello, media"))
|
||||
if server != "warp.example" {
|
||||
t.Errorf("uploaded to %q", server)
|
||||
}
|
||||
|
||||
download := "/_matrix/client/v1/media/download/" + server + "/" + id
|
||||
rec = getWithToken(t, srv, download, token)
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != "hello, media" {
|
||||
t.Fatalf("download: %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
for header, want := range map[string]string{
|
||||
"Content-Type": "text/plain",
|
||||
"Content-Disposition": "inline; filename=hello.txt",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Cache-Control": "private, max-age=31536000, immutable",
|
||||
} {
|
||||
if got := rec.Header().Get(header); got != want {
|
||||
t.Errorf("%s = %q, want %q", header, got, want)
|
||||
}
|
||||
}
|
||||
if csp := rec.Header().Get("Content-Security-Policy"); !strings.HasPrefix(csp, "sandbox;") {
|
||||
t.Errorf("Content-Security-Policy = %q", csp)
|
||||
}
|
||||
|
||||
// The authenticated endpoints need a token.
|
||||
if rec := get(t, srv, http.MethodGet, download); rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("download without a token: %d", rec.Code)
|
||||
}
|
||||
// The deprecated unauthenticated endpoint is off by default.
|
||||
if rec := get(t, srv, http.MethodGet, "/_matrix/media/v3/download/"+server+"/"+id); rec.Code != http.StatusNotFound ||
|
||||
!strings.Contains(rec.Body.String(), "M_UNRECOGNIZED") {
|
||||
t.Errorf("legacy download: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// A file name in the path renames the download.
|
||||
rec = getWithToken(t, srv, download+"/other.txt", token)
|
||||
if got := rec.Header().Get("Content-Disposition"); got != "inline; filename=other.txt" {
|
||||
t.Errorf("renamed download: Content-Disposition = %q", got)
|
||||
}
|
||||
|
||||
// Ranges and conditional requests come with http.ServeContent.
|
||||
rec = send(t, srv, http.MethodGet, download, "", nil, token, http.Header{"Range": {"bytes=0-4"}})
|
||||
if rec.Code != http.StatusPartialContent || rec.Body.String() != "hello" {
|
||||
t.Errorf("range: %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
etag := getWithToken(t, srv, download, token).Header().Get("ETag")
|
||||
rec = send(t, srv, http.MethodGet, download, "", nil, token, http.Header{"If-None-Match": {etag}})
|
||||
if rec.Code != http.StatusNotModified {
|
||||
t.Errorf("If-None-Match: %d", rec.Code)
|
||||
}
|
||||
|
||||
// An HTML upload is served as an attachment, never as a page.
|
||||
_, page := uploadThrough(t, srv, token, "text/html", "page.html", []byte("<script>alert(1)</script>"))
|
||||
rec = getWithToken(t, srv, "/_matrix/client/v1/media/download/warp.example/"+page, token)
|
||||
if got := rec.Header().Get("Content-Disposition"); !strings.HasPrefix(got, "attachment") {
|
||||
t.Errorf("HTML Content-Disposition = %q", got)
|
||||
}
|
||||
|
||||
rec = getWithToken(t, srv, "/_matrix/client/v1/media/download/warp.example/doesnotexist", token)
|
||||
if rec.Code != http.StatusNotFound || !strings.Contains(rec.Body.String(), "M_NOT_FOUND") {
|
||||
t.Errorf("unknown media: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaThumbnailsThroughTheAPI(t *testing.T) {
|
||||
srv := newMediaServer(t, nil)
|
||||
token := registerThrough(t, srv, "alice")
|
||||
server, id := uploadThrough(t, srv, token, "image/png", "", pngOf(t, 400, 300))
|
||||
|
||||
thumb := "/_matrix/client/v1/media/thumbnail/" + server + "/" + id
|
||||
rec := getWithToken(t, srv, thumb+"?width=64&height=64&method=crop", token)
|
||||
if rec.Code != http.StatusOK || rec.Header().Get("Content-Type") != "image/png" {
|
||||
t.Fatalf("thumbnail: %d %s %s", rec.Code, rec.Header().Get("Content-Type"), rec.Body.String())
|
||||
}
|
||||
img, err := png.Decode(rec.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("thumbnail does not decode: %v", err)
|
||||
}
|
||||
if b := img.Bounds(); b.Dx() != 96 || b.Dy() != 96 {
|
||||
t.Errorf("thumbnail is %v, want 96 × 96", b)
|
||||
}
|
||||
|
||||
for query, want := range map[string]string{
|
||||
"": "M_MISSING_PARAM",
|
||||
"?width=0&height=10": "M_INVALID_PARAM",
|
||||
"?width=10&height=10&method=zap": "M_INVALID_PARAM",
|
||||
} {
|
||||
rec := getWithToken(t, srv, thumb+query, token)
|
||||
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), want) {
|
||||
t.Errorf("thumbnail%s: %d %s, want %s", query, rec.Code, rec.Body.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
_, text := uploadThrough(t, srv, token, "text/plain", "", []byte("words"))
|
||||
rec = getWithToken(t, srv, "/_matrix/client/v1/media/thumbnail/warp.example/"+text+"?width=32&height=32", token)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("thumbnail of text: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsyncUpload(t *testing.T) {
|
||||
srv := newMediaServer(t, nil)
|
||||
alice := registerThrough(t, srv, "alice")
|
||||
bob := registerThrough(t, srv, "bob")
|
||||
|
||||
rec := post(t, srv, "/_matrix/media/v1/create", map[string]any{}, alice)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("create: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
created := decode[map[string]any](t, rec)
|
||||
mxc, _ := created["content_uri"].(string)
|
||||
if _, ok := created["unused_expires_at"].(float64); !ok {
|
||||
t.Errorf("create response lacks unused_expires_at: %v", created)
|
||||
}
|
||||
server, id := splitMXC(t, mxc)
|
||||
|
||||
download := "/_matrix/client/v1/media/download/" + server + "/" + id
|
||||
rec = getWithToken(t, srv, download+"?timeout_ms=0", alice)
|
||||
if rec.Code != http.StatusGatewayTimeout || !strings.Contains(rec.Body.String(), "M_NOT_YET_UPLOADED") {
|
||||
t.Errorf("download before upload: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
put := "/_matrix/media/v3/upload/" + server + "/" + id
|
||||
if rec := send(t, srv, http.MethodPut, put, "text/plain", []byte("bob's"), bob, nil); rec.Code != http.StatusForbidden {
|
||||
t.Errorf("upload to another user's ID: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec := send(t, srv, http.MethodPut, put, "text/plain", []byte("later"), alice, nil); rec.Code != http.StatusOK {
|
||||
t.Fatalf("upload: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = send(t, srv, http.MethodPut, put, "text/plain", []byte("again"), alice, nil)
|
||||
if rec.Code != http.StatusConflict || !strings.Contains(rec.Body.String(), "M_CANNOT_OVERWRITE_MEDIA") {
|
||||
t.Errorf("second upload: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec := getWithToken(t, srv, download, bob); rec.Code != http.StatusOK || rec.Body.String() != "later" {
|
||||
t.Errorf("download: %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadLimit(t *testing.T) {
|
||||
srv := newMediaServer(t, func(cfg *config.Config) { cfg.Media.MaxUploadBytes = 8 })
|
||||
token := registerThrough(t, srv, "alice")
|
||||
|
||||
rec := send(t, srv, http.MethodPost, "/_matrix/media/v3/upload", "text/plain", []byte("more than eight"), token, nil)
|
||||
if rec.Code != http.StatusRequestEntityTooLarge || !strings.Contains(rec.Body.String(), "M_TOO_LARGE") {
|
||||
t.Errorf("oversized upload: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// Without a Content-Length the blob store cuts it off instead.
|
||||
req := httptest.NewRequest(http.MethodPost, "/_matrix/media/v3/upload", strings.NewReader("more than eight"))
|
||||
req.ContentLength = -1
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Errorf("oversized chunked upload: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyMediaDownloads(t *testing.T) {
|
||||
srv := newMediaServer(t, func(cfg *config.Config) { cfg.Media.LegacyUnauthenticatedDownloads = true })
|
||||
token := registerThrough(t, srv, "alice")
|
||||
server, id := uploadThrough(t, srv, token, "image/png", "", pngOf(t, 10, 10))
|
||||
|
||||
if rec := get(t, srv, http.MethodGet, "/_matrix/media/v3/download/"+server+"/"+id); rec.Code != http.StatusOK {
|
||||
t.Errorf("legacy download: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if rec := get(t, srv, http.MethodGet, "/_matrix/media/v3/thumbnail/"+server+"/"+id+"?width=32&height=32"); rec.Code != http.StatusOK {
|
||||
t.Errorf("legacy thumbnail: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaOverFederation(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("starts two servers")
|
||||
}
|
||||
nodes := newFederation(t, "hs-a.example", "hs-b.example")
|
||||
a, b := nodes["hs-a.example"].srv, nodes["hs-b.example"].srv
|
||||
alice := registerThrough(t, a, "alice")
|
||||
bob := registerThrough(t, b, "bob")
|
||||
|
||||
server, id := uploadThrough(t, a, alice, "image/png", "cat.png", pngOf(t, 300, 200))
|
||||
|
||||
// Bob's server fetches Alice's upload from her server.
|
||||
download := "/_matrix/client/v1/media/download/" + server + "/" + id
|
||||
rec := getWithToken(t, b, download, bob)
|
||||
if rec.Code != http.StatusOK || rec.Header().Get("Content-Type") != "image/png" {
|
||||
t.Fatalf("remote download: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := rec.Header().Get("Content-Disposition"); got != "inline; filename=cat.png" {
|
||||
t.Errorf("remote download kept no file name: %q", got)
|
||||
}
|
||||
if _, err := png.Decode(rec.Body); err != nil {
|
||||
t.Errorf("remote download is not the PNG: %v", err)
|
||||
}
|
||||
|
||||
// A thumbnail of media that is cached is made locally; of media that
|
||||
// is not, it is fetched.
|
||||
_, other := uploadThrough(t, a, alice, "image/png", "", pngOf(t, 500, 500))
|
||||
rec = getWithToken(t, b, "/_matrix/client/v1/media/thumbnail/"+server+"/"+other+"?width=32&height=32&method=crop", bob)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("remote thumbnail: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if img, err := png.Decode(rec.Body); err != nil || img.Bounds().Dx() != 32 {
|
||||
t.Errorf("remote thumbnail: %v, %v", img, err)
|
||||
}
|
||||
|
||||
// Unknown media on the other server is a 404 here too.
|
||||
rec = getWithToken(t, b, "/_matrix/client/v1/media/download/"+server+"/doesnotexist", bob)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("unknown remote media: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// With B's federation off, what B has cached is still served and
|
||||
// nothing else is fetched.
|
||||
if err := b.Settings().SetFederationEnabled(t.Context(), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec := getWithToken(t, b, download, bob); rec.Code != http.StatusOK {
|
||||
t.Errorf("cached remote download with federation off: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
_, uncached := uploadThrough(t, a, alice, "text/plain", "", []byte("never fetched"))
|
||||
rec = getWithToken(t, b, "/_matrix/client/v1/media/download/"+server+"/"+uncached, bob)
|
||||
if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "Federation is disabled") {
|
||||
t.Errorf("uncached remote download with federation off: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// Another server is only served this server's own media, and only
|
||||
// over federation.
|
||||
if rec := get(t, a, http.MethodGet, "/_matrix/federation/v1/media/download/"+id); rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("unsigned federation download: %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
//
|
||||
// Keep it honest: a version belongs here once the endpoints a client
|
||||
// actually uses from it work. Accounts, sessions, devices, profiles,
|
||||
// account data, rooms and — with Phase 4 — the event stream all do, which
|
||||
// is enough for a client to be used. What is still missing is end-to-end
|
||||
// encryption (Phase 7), media (Phase 6) and push (Phase 8); a client
|
||||
// discovers each of those from the endpoint it needs rather than from this
|
||||
// list, which is why their absence does not make a version listed here a
|
||||
// lie.
|
||||
// account data, rooms, the event stream and — with Phase 6 — media all
|
||||
// do, which is enough for a client to be used. Advertising v1.11 or later
|
||||
// is also what makes a client use the authenticated media endpoints, the
|
||||
// only download endpoints Warp serves by default. What is still missing
|
||||
// is end-to-end encryption (Phase 7) and push (Phase 8); a client
|
||||
// discovers each of those from the endpoint it needs rather than from
|
||||
// this list, which is why their absence does not make a version listed
|
||||
// here a lie.
|
||||
var SpecVersions = []string{"v1.13", "v1.14"}
|
||||
|
||||
// UnstableFeatures advertises MSC support. Simplified Sliding Sync
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/dariuszjaniak/warp/server/internal/federationapi"
|
||||
"github.com/dariuszjaniak/warp/server/internal/httputil"
|
||||
"github.com/dariuszjaniak/warp/server/internal/matrix/signing"
|
||||
"github.com/dariuszjaniak/warp/server/internal/mediaapi"
|
||||
"github.com/dariuszjaniak/warp/server/internal/metrics"
|
||||
"github.com/dariuszjaniak/warp/server/internal/roomserver"
|
||||
"github.com/dariuszjaniak/warp/server/internal/settings"
|
||||
@@ -45,6 +46,7 @@ type Server struct {
|
||||
users *userapi.API
|
||||
rooms *roomserver.API
|
||||
sync *syncapi.API
|
||||
media *mediaapi.API
|
||||
clientAPI *clientapi.ClientAPI
|
||||
adminAPI *adminapi.AdminAPI
|
||||
|
||||
@@ -186,6 +188,15 @@ func New(ctx context.Context, cfg *config.Config, opts Options) (*Server, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The media repository needs no key: uploads and downloads of local
|
||||
// media are not signed. Fetching other servers' media is, and is
|
||||
// handed the federation client below once there is one.
|
||||
s.media, err = newMedia(cfg, s.db, log)
|
||||
if err != nil {
|
||||
_ = s.closeDB()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The roomserver needs the signing key: it is the only thing in Warp
|
||||
// that creates an event, and an event has to be signed. `warp migrate`
|
||||
// and the Phase 1 tests start without a key, and then start without
|
||||
@@ -224,6 +235,7 @@ func New(ctx context.Context, cfg *config.Config, opts Options) (*Server, error)
|
||||
Enabled: settingsService.FederationEnabled,
|
||||
Policy: s.federation.Policy(),
|
||||
})
|
||||
s.media.SetRemote(s.fedClient)
|
||||
s.keyRing = federation.NewKeyRing(federation.KeyRingOptions{
|
||||
ServerName: cfg.Global.ServerName,
|
||||
Key: s.signingKey,
|
||||
@@ -286,6 +298,7 @@ func New(ctx context.Context, cfg *config.Config, opts Options) (*Server, error)
|
||||
Ephemeral: s.sync,
|
||||
Profiles: s.users,
|
||||
Store: s.db.Federation(),
|
||||
Media: s.media,
|
||||
Logger: log,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -307,18 +320,20 @@ func New(ctx context.Context, cfg *config.Config, opts Options) (*Server, error)
|
||||
}
|
||||
|
||||
s.clientAPI, err = clientapi.New(clientapi.Options{
|
||||
ServerName: cfg.Global.ServerName,
|
||||
ClientBaseURL: cfg.ClientBaseURL(),
|
||||
Users: s.users,
|
||||
Rooms: s.rooms,
|
||||
Sync: s.sync,
|
||||
Settings: settingsService,
|
||||
Federation: s.federation,
|
||||
RemoteProfiles: remoteProfiles,
|
||||
Registration: cfg.Registration,
|
||||
RateLimit: cfg.ClientAPI.RateLimit,
|
||||
TrustXForwardedFor: cfg.ClientAPI.TrustXForwardedFor,
|
||||
Logger: log,
|
||||
ServerName: cfg.Global.ServerName,
|
||||
ClientBaseURL: cfg.ClientBaseURL(),
|
||||
Users: s.users,
|
||||
Rooms: s.rooms,
|
||||
Sync: s.sync,
|
||||
Media: s.media,
|
||||
LegacyMediaDownloads: cfg.Media.LegacyUnauthenticatedDownloads,
|
||||
Settings: settingsService,
|
||||
Federation: s.federation,
|
||||
RemoteProfiles: remoteProfiles,
|
||||
Registration: cfg.Registration,
|
||||
RateLimit: cfg.ClientAPI.RateLimit,
|
||||
TrustXForwardedFor: cfg.ClientAPI.TrustXForwardedFor,
|
||||
Logger: log,
|
||||
})
|
||||
if err != nil {
|
||||
_ = s.closeDB()
|
||||
@@ -399,6 +414,9 @@ func (s *Server) Rooms() *roomserver.API { return s.rooms }
|
||||
// without a signing key.
|
||||
func (s *Server) Sync() *syncapi.API { return s.sync }
|
||||
|
||||
// Media exposes the media repository.
|
||||
func (s *Server) Media() *mediaapi.API { return s.media }
|
||||
|
||||
// SigningKey returns the server signing key, or nil when it was skipped.
|
||||
func (s *Server) SigningKey() *signing.KeyPair { return s.signingKey }
|
||||
|
||||
|
||||
Reference in new issue
Block a user