Compare commits

...

8 commits

Author SHA1 Message Date
Till Faelligen e8b69fba03
Set correct Content-Type 2024-07-02 08:52:28 +02:00
Till Faelligen 53e545ddc5
Update GMSL 2024-07-02 08:51:16 +02:00
Till Faelligen 51f6dcbdc5
Make the linter happy 2024-06-19 12:05:30 +02:00
Till Faelligen aeee46bb71
Announce support for MSC3916, fix wrong JSON message on error 2024-06-19 12:02:47 +02:00
Till Faelligen 89d64c6dca
Add support for getting authenticated federation media requests 2024-06-18 15:20:10 +02:00
Till Faelligen 29ee5401ee
Add support for authenticated federation media requests 2024-06-18 12:36:35 +02:00
Till Faelligen ccd337e1d4
Add authentication to MakeHTMLAPI and use it for media requests 2024-06-17 11:33:03 +02:00
Till Faelligen 40eb7cbeef
Use httputil.Routers for the media API 2024-06-17 10:25:41 +02:00
9 changed files with 301 additions and 37 deletions

View file

@ -94,6 +94,7 @@ func Setup(
unstableFeatures := map[string]bool{ unstableFeatures := map[string]bool{
"org.matrix.e2e_cross_signing": true, "org.matrix.e2e_cross_signing": true,
"org.matrix.msc2285.stable": true, "org.matrix.msc2285.stable": true,
"org.matrix.msc3916": true,
} }
for _, msc := range cfg.MSCs.MSCs { for _, msc := range cfg.MSCs.MSCs {
unstableFeatures["org.matrix."+msc] = true unstableFeatures["org.matrix."+msc] = true
@ -732,7 +733,7 @@ func Setup(
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
v3mux.Handle("/auth/{authType}/fallback/web", v3mux.Handle("/auth/{authType}/fallback/web",
httputil.MakeHTMLAPI("auth_fallback", enableMetrics, func(w http.ResponseWriter, req *http.Request) { httputil.MakeHTMLAPI("auth_fallback", userAPI, enableMetrics, func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req) vars := mux.Vars(req)
AuthFallback(w, req, vars["authType"], cfg) AuthFallback(w, req, vars["authType"], cfg)
}), }),

View file

@ -16,6 +16,7 @@ package routing
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sync" "sync"
@ -678,6 +679,53 @@ func MakeFedAPI(
return httputil.MakeExternalAPI(metricsName, h) return httputil.MakeExternalAPI(metricsName, h)
} }
// MakeFedAPI makes an http.Handler that checks matrix federation authentication.
func MakeFedAPIHTML(
serverName spec.ServerName,
isLocalServerName func(spec.ServerName) bool,
keyRing gomatrixserverlib.JSONVerifier,
f func(http.ResponseWriter, *http.Request),
) http.Handler {
h := func(w http.ResponseWriter, req *http.Request) {
fedReq, errResp := fclient.VerifyHTTPRequest(
req, time.Now(), serverName, isLocalServerName, keyRing,
)
enc := json.NewEncoder(w)
logger := util.GetLogger(req.Context())
if fedReq == nil {
logger.Debugf("VerifyUserFromRequest %s -> HTTP %d", req.RemoteAddr, errResp.Code)
w.WriteHeader(errResp.Code)
if err := enc.Encode(errResp); err != nil {
logger.WithError(err).Error("failed to encode JSON response")
}
return
}
// add the user to Sentry, if enabled
hub := sentry.GetHubFromContext(req.Context())
if hub != nil {
// clone the hub, so we don't send garbage events with e.g. mismatching rooms/event_ids
hub = hub.Clone()
hub.Scope().SetTag("origin", string(fedReq.Origin()))
hub.Scope().SetTag("uri", fedReq.RequestURI())
}
defer func() {
if r := recover(); r != nil {
if hub != nil {
hub.CaptureException(fmt.Errorf("%s panicked", req.URL.Path))
}
// re-panic to return the 500
panic(r)
}
}()
f(w, req)
}
return http.HandlerFunc(h)
}
type FederationWakeups struct { type FederationWakeups struct {
FsAPI *fedInternal.FederationInternalAPI FsAPI *fedInternal.FederationInternalAPI
origins sync.Map origins sync.Map

2
go.mod
View file

@ -22,7 +22,7 @@ require (
github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530
github.com/matrix-org/gomatrixserverlib v0.0.0-20240328203753-c2391f7113a5 github.com/matrix-org/gomatrixserverlib v0.0.0-20240618131621-ba4c4992f66d
github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7 github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7
github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 github.com/matrix-org/util v0.0.0-20221111132719-399730281e66
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22

4
go.sum
View file

@ -211,8 +211,8 @@ github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 h1:s7fexw
github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1:e+cg2q7C7yE5QnAXgzo512tgFh1RbQLC0+jozuegKgo= github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1:e+cg2q7C7yE5QnAXgzo512tgFh1RbQLC0+jozuegKgo=
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U=
github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
github.com/matrix-org/gomatrixserverlib v0.0.0-20240328203753-c2391f7113a5 h1:GuxmpyjZQoqb6UFQgKq8Td3wIITlXln/sItqp1jbTTA= github.com/matrix-org/gomatrixserverlib v0.0.0-20240618131621-ba4c4992f66d h1:drDloOznSn5b9vhrsK5LNziiPECZDvgqwYim6s4hmqU=
github.com/matrix-org/gomatrixserverlib v0.0.0-20240328203753-c2391f7113a5/go.mod h1:HZGsVJ3bUE+DkZtufkH9H0mlsvbhEGK5CpX0Zlavylg= github.com/matrix-org/gomatrixserverlib v0.0.0-20240618131621-ba4c4992f66d/go.mod h1:HZGsVJ3bUE+DkZtufkH9H0mlsvbhEGK5CpX0Zlavylg=
github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7 h1:6t8kJr8i1/1I5nNttw6nn1ryQJgzVlBmSGgPiiaTdw4= github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7 h1:6t8kJr8i1/1I5nNttw6nn1ryQJgzVlBmSGgPiiaTdw4=
github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7/go.mod h1:ReWMS/LoVnOiRAdq9sNUC2NZnd1mZkMNB52QhpTRWjg= github.com/matrix-org/pinecone v0.11.1-0.20230810010612-ea4c33717fd7/go.mod h1:ReWMS/LoVnOiRAdq9sNUC2NZnd1mZkMNB52QhpTRWjg=
github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y= github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y=

View file

@ -15,6 +15,7 @@
package httputil package httputil
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -44,6 +45,7 @@ type BasicAuth struct {
type AuthAPIOpts struct { type AuthAPIOpts struct {
GuestAccessAllowed bool GuestAccessAllowed bool
WithAuth bool
} }
// AuthAPIOption is an option to MakeAuthAPI to add additional checks (e.g. guest access) to verify // AuthAPIOption is an option to MakeAuthAPI to add additional checks (e.g. guest access) to verify
@ -57,6 +59,13 @@ func WithAllowGuests() AuthAPIOption {
} }
} }
// WithAuth is an option to MakeHTMLAPI to add authentication.
func WithAuth() AuthAPIOption {
return func(opts *AuthAPIOpts) {
opts.WithAuth = true
}
}
// MakeAuthAPI turns a util.JSONRequestHandler function into an http.Handler which authenticates the request. // MakeAuthAPI turns a util.JSONRequestHandler function into an http.Handler which authenticates the request.
func MakeAuthAPI( func MakeAuthAPI(
metricsName string, userAPI userapi.QueryAcccessTokenAPI, metricsName string, userAPI userapi.QueryAcccessTokenAPI,
@ -199,11 +208,31 @@ func MakeExternalAPI(metricsName string, f func(*http.Request) util.JSONResponse
// MakeHTMLAPI adds Span metrics to the HTML Handler function // MakeHTMLAPI adds Span metrics to the HTML Handler function
// This is used to serve HTML alongside JSON error messages // This is used to serve HTML alongside JSON error messages
func MakeHTMLAPI(metricsName string, enableMetrics bool, f func(http.ResponseWriter, *http.Request)) http.Handler { func MakeHTMLAPI(metricsName string, userAPI userapi.QueryAcccessTokenAPI, enableMetrics bool, f func(http.ResponseWriter, *http.Request), checks ...AuthAPIOption) http.Handler {
withSpan := func(w http.ResponseWriter, req *http.Request) { withSpan := func(w http.ResponseWriter, req *http.Request) {
trace, ctx := internal.StartTask(req.Context(), metricsName) trace, ctx := internal.StartTask(req.Context(), metricsName)
defer trace.EndTask() defer trace.EndTask()
req = req.WithContext(ctx) req = req.WithContext(ctx)
// apply additional checks, if any
opts := AuthAPIOpts{}
for _, opt := range checks {
opt(&opts)
}
if opts.WithAuth {
logger := util.GetLogger(req.Context())
_, jsonErr := auth.VerifyUserFromRequest(req, userAPI)
if jsonErr != nil {
logger.Debugf("VerifyUserFromRequest %s -> HTTP %d", req.RemoteAddr, jsonErr.Code)
w.WriteHeader(jsonErr.Code)
if err := json.NewEncoder(w).Encode(jsonErr.JSON); err != nil {
logger.WithError(err).Error("failed to encode JSON response")
}
return
}
}
f(w, req) f(w, req)
} }

View file

@ -15,23 +15,26 @@
package mediaapi package mediaapi
import ( import (
"github.com/gorilla/mux" "github.com/matrix-org/dendrite/internal/httputil"
"github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/internal/sqlutil"
"github.com/matrix-org/dendrite/mediaapi/routing" "github.com/matrix-org/dendrite/mediaapi/routing"
"github.com/matrix-org/dendrite/mediaapi/storage" "github.com/matrix-org/dendrite/mediaapi/storage"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api" userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/fclient" "github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component. // AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component.
func AddPublicRoutes( func AddPublicRoutes(
mediaRouter *mux.Router, routers httputil.Routers,
cm *sqlutil.Connections, cm *sqlutil.Connections,
cfg *config.Dendrite, cfg *config.Dendrite,
userAPI userapi.MediaUserAPI, userAPI userapi.MediaUserAPI,
client *fclient.Client, client *fclient.Client,
fedClient fclient.FederationClient,
keyRing gomatrixserverlib.JSONVerifier,
) { ) {
mediaDB, err := storage.NewMediaAPIDatasource(cm, &cfg.MediaAPI.Database) mediaDB, err := storage.NewMediaAPIDatasource(cm, &cfg.MediaAPI.Database)
if err != nil { if err != nil {
@ -39,6 +42,6 @@ func AddPublicRoutes(
} }
routing.Setup( routing.Setup(
mediaRouter, cfg, mediaDB, userAPI, client, routers, cfg, mediaDB, userAPI, client, fedClient, keyRing,
) )
} }

View file

@ -21,7 +21,9 @@ import (
"io" "io"
"io/fs" "io/fs"
"mime" "mime"
"mime/multipart"
"net/http" "net/http"
"net/textproto"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -31,6 +33,7 @@ import (
"sync" "sync"
"unicode" "unicode"
"github.com/google/uuid"
"github.com/matrix-org/dendrite/mediaapi/fileutils" "github.com/matrix-org/dendrite/mediaapi/fileutils"
"github.com/matrix-org/dendrite/mediaapi/storage" "github.com/matrix-org/dendrite/mediaapi/storage"
"github.com/matrix-org/dendrite/mediaapi/thumbnailer" "github.com/matrix-org/dendrite/mediaapi/thumbnailer"
@ -61,6 +64,9 @@ type downloadRequest struct {
ThumbnailSize types.ThumbnailSize ThumbnailSize types.ThumbnailSize
Logger *log.Entry Logger *log.Entry
DownloadFilename string DownloadFilename string
forFederation bool // whether we need to return a multipart/mixed response
fedClient fclient.FederationClient
origin spec.ServerName
} }
// Taken from: https://github.com/matrix-org/synapse/blob/c3627d0f99ed5a23479305dc2bd0e71ca25ce2b1/synapse/media/_base.py#L53C1-L84 // Taken from: https://github.com/matrix-org/synapse/blob/c3627d0f99ed5a23479305dc2bd0e71ca25ce2b1/synapse/media/_base.py#L53C1-L84
@ -111,11 +117,17 @@ func Download(
cfg *config.MediaAPI, cfg *config.MediaAPI,
db storage.Database, db storage.Database,
client *fclient.Client, client *fclient.Client,
fedClient fclient.FederationClient,
activeRemoteRequests *types.ActiveRemoteRequests, activeRemoteRequests *types.ActiveRemoteRequests,
activeThumbnailGeneration *types.ActiveThumbnailGeneration, activeThumbnailGeneration *types.ActiveThumbnailGeneration,
isThumbnailRequest bool, isThumbnailRequest bool,
customFilename string, customFilename string,
forFederation bool,
) { ) {
// This happens if we call Download for a federation request
if forFederation && origin == "" {
origin = cfg.Matrix.ServerName
}
dReq := &downloadRequest{ dReq := &downloadRequest{
MediaMetadata: &types.MediaMetadata{ MediaMetadata: &types.MediaMetadata{
MediaID: mediaID, MediaID: mediaID,
@ -127,6 +139,9 @@ func Download(
"MediaID": mediaID, "MediaID": mediaID,
}), }),
DownloadFilename: customFilename, DownloadFilename: customFilename,
forFederation: forFederation,
origin: cfg.Matrix.ServerName,
fedClient: fedClient,
} }
if dReq.IsThumbnailRequest { if dReq.IsThumbnailRequest {
@ -355,7 +370,7 @@ func (r *downloadRequest) respondFromLocalFile(
}).Trace("Responding with file") }).Trace("Responding with file")
responseFile = file responseFile = file
responseMetadata = r.MediaMetadata responseMetadata = r.MediaMetadata
if err := r.addDownloadFilenameToHeaders(w, responseMetadata); err != nil { if err = r.addDownloadFilenameToHeaders(w, responseMetadata); err != nil {
return nil, err return nil, err
} }
} }
@ -369,8 +384,49 @@ func (r *downloadRequest) respondFromLocalFile(
" object-src 'self';" " object-src 'self';"
w.Header().Set("Content-Security-Policy", contentSecurityPolicy) w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
if _, err := io.Copy(w, responseFile); err != nil { if !r.forFederation {
return nil, fmt.Errorf("io.Copy: %w", err) if _, err = io.Copy(w, responseFile); err != nil {
return nil, fmt.Errorf("io.Copy: %w", err)
}
} else {
// Update the header to be multipart/mixed; boundary=$randomBoundary
boundary := uuid.NewString()
w.Header().Set("Content-Type", "multipart/mixed; boundary="+boundary)
w.Header().Del("Content-Length") // let Go handle the content length
w.Header().Del("Content-Security-Policy") // S-S request, so does not really matter?
mw := multipart.NewWriter(w)
defer func() {
if err = mw.Close(); err != nil {
r.Logger.WithError(err).Error("Failed to close multipart writer")
}
}()
if err = mw.SetBoundary(boundary); err != nil {
return nil, fmt.Errorf("failed to set multipart boundary: %w", err)
}
// JSON object part
jsonWriter, err := mw.CreatePart(textproto.MIMEHeader{
"Content-Type": {"application/json"},
})
if err != nil {
return nil, fmt.Errorf("failed to create json writer: %w", err)
}
if _, err = jsonWriter.Write([]byte("{}")); err != nil {
return nil, fmt.Errorf("failed to write to json writer: %w", err)
}
// media part
mediaWriter, err := mw.CreatePart(textproto.MIMEHeader{
"Content-Type": {string(responseMetadata.ContentType)},
})
if err != nil {
return nil, fmt.Errorf("failed to create media writer: %w", err)
}
if _, err = io.Copy(mediaWriter, responseFile); err != nil {
return nil, fmt.Errorf("failed to write to media writer: %w", err)
}
} }
return responseMetadata, nil return responseMetadata, nil
} }
@ -722,8 +778,7 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(
return nil return nil
} }
func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string, body *io.ReadCloser, maxFileSizeBytes config.FileSizeBytes) (int64, io.Reader, error) { func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string, reader io.ReadCloser, maxFileSizeBytes config.FileSizeBytes) (int64, io.Reader, error) {
reader := *body
var contentLength int64 var contentLength int64
if contentLengthHeader != "" { if contentLengthHeader != "" {
@ -742,7 +797,7 @@ func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string,
// We successfully parsed the Content-Length, so we'll return a limited // We successfully parsed the Content-Length, so we'll return a limited
// reader that restricts us to reading only up to this size. // reader that restricts us to reading only up to this size.
reader = io.NopCloser(io.LimitReader(*body, parsedLength)) reader = io.NopCloser(io.LimitReader(reader, parsedLength))
contentLength = parsedLength contentLength = parsedLength
} else { } else {
// Content-Length header is missing. If we have a maximum file size // Content-Length header is missing. If we have a maximum file size
@ -751,7 +806,7 @@ func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string,
// ultimately it will get rewritten later when the temp file is written // ultimately it will get rewritten later when the temp file is written
// to disk. // to disk.
if maxFileSizeBytes > 0 { if maxFileSizeBytes > 0 {
reader = io.NopCloser(io.LimitReader(*body, int64(maxFileSizeBytes))) reader = io.NopCloser(io.LimitReader(reader, int64(maxFileSizeBytes)))
} }
contentLength = 0 contentLength = 0
} }
@ -759,6 +814,7 @@ func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string,
return contentLength, reader, nil return contentLength, reader, nil
} }
// nolint: gocyclo
func (r *downloadRequest) fetchRemoteFile( func (r *downloadRequest) fetchRemoteFile(
ctx context.Context, ctx context.Context,
client *fclient.Client, client *fclient.Client,
@ -767,19 +823,69 @@ func (r *downloadRequest) fetchRemoteFile(
) (types.Path, bool, error) { ) (types.Path, bool, error) {
r.Logger.Debug("Fetching remote file") r.Logger.Debug("Fetching remote file")
// create request for remote file // Attempt to download via authenticated media endpoint
resp, err := client.CreateMediaDownloadRequest(ctx, r.MediaMetadata.Origin, string(r.MediaMetadata.MediaID)) isMultiPart := true
resp, err := r.fedClient.DownloadMedia(ctx, r.origin, r.MediaMetadata.Origin, string(r.MediaMetadata.MediaID))
if err != nil || (resp != nil && resp.StatusCode != http.StatusOK) { if err != nil || (resp != nil && resp.StatusCode != http.StatusOK) {
if resp != nil && resp.StatusCode == http.StatusNotFound { isMultiPart = false
return "", false, fmt.Errorf("File with media ID %q does not exist on %s", r.MediaMetadata.MediaID, r.MediaMetadata.Origin) // try again on the unauthed endpoint
// create request for remote file
resp, err = client.CreateMediaDownloadRequest(ctx, r.MediaMetadata.Origin, string(r.MediaMetadata.MediaID))
if err != nil || (resp != nil && resp.StatusCode != http.StatusOK) {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return "", false, fmt.Errorf("File with media ID %q does not exist on %s", r.MediaMetadata.MediaID, r.MediaMetadata.Origin)
}
return "", false, fmt.Errorf("file with media ID %q could not be downloaded from %s", r.MediaMetadata.MediaID, r.MediaMetadata.Origin)
} }
return "", false, fmt.Errorf("file with media ID %q could not be downloaded from %s", r.MediaMetadata.MediaID, r.MediaMetadata.Origin)
} }
defer resp.Body.Close() // nolint: errcheck defer resp.Body.Close() // nolint: errcheck
// The reader returned here will be limited either by the Content-Length // If this wasn't a multipart response, set the Content-Type now. Will be overwritten
// and/or the configured maximum media size. // by the multipart Content-Type below.
contentLength, reader, parseErr := r.GetContentLengthAndReader(resp.Header.Get("Content-Length"), &resp.Body, maxFileSizeBytes) r.MediaMetadata.ContentType = types.ContentType(resp.Header.Get("Content-Type"))
var contentLength int64
var reader io.Reader
var parseErr error
if isMultiPart {
r.Logger.Info("Downloaded file using authenticated endpoint")
var params map[string]string
_, params, err = mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
panic(err)
}
if params["boundary"] == "" {
return "", false, fmt.Errorf("no boundary header found on %s", r.MediaMetadata.Origin)
}
mr := multipart.NewReader(resp.Body, params["boundary"])
first := true
for {
var p *multipart.Part
p, err = mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
return "", false, err
}
if !first {
readCloser := io.NopCloser(p)
contentLength, reader, parseErr = r.GetContentLengthAndReader(p.Header.Get("Content-Length"), readCloser, maxFileSizeBytes)
// For multipart requests, we need to get the Content-Type of the second part, which is the actual media
r.MediaMetadata.ContentType = types.ContentType(p.Header.Get("Content-Type"))
break
}
first = false
}
} else {
// The reader returned here will be limited either by the Content-Length
// and/or the configured maximum media size.
contentLength, reader, parseErr = r.GetContentLengthAndReader(resp.Header.Get("Content-Length"), resp.Body, maxFileSizeBytes)
}
if parseErr != nil { if parseErr != nil {
return "", false, parseErr return "", false, parseErr
} }
@ -790,7 +896,6 @@ func (r *downloadRequest) fetchRemoteFile(
} }
r.MediaMetadata.FileSizeBytes = types.FileSizeBytes(contentLength) r.MediaMetadata.FileSizeBytes = types.FileSizeBytes(contentLength)
r.MediaMetadata.ContentType = types.ContentType(resp.Header.Get("Content-Type"))
dispositionHeader := resp.Header.Get("Content-Disposition") dispositionHeader := resp.Header.Get("Content-Disposition")
if _, params, e := mime.ParseMediaType(dispositionHeader); e == nil { if _, params, e := mime.ParseMediaType(dispositionHeader); e == nil {

View file

@ -20,11 +20,13 @@ import (
"strings" "strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/matrix-org/dendrite/federationapi/routing"
"github.com/matrix-org/dendrite/internal/httputil" "github.com/matrix-org/dendrite/internal/httputil"
"github.com/matrix-org/dendrite/mediaapi/storage" "github.com/matrix-org/dendrite/mediaapi/storage"
"github.com/matrix-org/dendrite/mediaapi/types" "github.com/matrix-org/dendrite/mediaapi/types"
"github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/config"
userapi "github.com/matrix-org/dendrite/userapi/api" userapi "github.com/matrix-org/dendrite/userapi/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/fclient" "github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/gomatrixserverlib/spec" "github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/util" "github.com/matrix-org/util"
@ -45,15 +47,19 @@ type configResponse struct {
// applied: // applied:
// nolint: gocyclo // nolint: gocyclo
func Setup( func Setup(
publicAPIMux *mux.Router, routers httputil.Routers,
cfg *config.Dendrite, cfg *config.Dendrite,
db storage.Database, db storage.Database,
userAPI userapi.MediaUserAPI, userAPI userapi.MediaUserAPI,
client *fclient.Client, client *fclient.Client,
federationClient fclient.FederationClient,
keyRing gomatrixserverlib.JSONVerifier,
) { ) {
rateLimits := httputil.NewRateLimits(&cfg.ClientAPI.RateLimiting) rateLimits := httputil.NewRateLimits(&cfg.ClientAPI.RateLimiting)
v3mux := publicAPIMux.PathPrefix("/{apiversion:(?:r0|v1|v3)}/").Subrouter() v3mux := routers.Media.PathPrefix("/{apiversion:(?:r0|v1|v3)}/").Subrouter()
v1mux := routers.Client.PathPrefix("/v1/media/").Subrouter()
v1fedMux := routers.Federation.PathPrefix("/v1/media/").Subrouter()
activeThumbnailGeneration := &types.ActiveThumbnailGeneration{ activeThumbnailGeneration := &types.ActiveThumbnailGeneration{
PathToResult: map[string]*types.ThumbnailGenerationResult{}, PathToResult: map[string]*types.ThumbnailGenerationResult{},
@ -90,33 +96,100 @@ func Setup(
MXCToResult: map[string]*types.RemoteRequestResult{}, MXCToResult: map[string]*types.RemoteRequestResult{},
} }
downloadHandler := makeDownloadAPI("download", &cfg.MediaAPI, rateLimits, db, client, activeRemoteRequests, activeThumbnailGeneration) downloadHandler := makeDownloadAPI("download_unauthed", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, false)
v3mux.Handle("/download/{serverName}/{mediaId}", downloadHandler).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/download/{serverName}/{mediaId}", downloadHandler).Methods(http.MethodGet, http.MethodOptions)
v3mux.Handle("/download/{serverName}/{mediaId}/{downloadName}", downloadHandler).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/download/{serverName}/{mediaId}/{downloadName}", downloadHandler).Methods(http.MethodGet, http.MethodOptions)
v3mux.Handle("/thumbnail/{serverName}/{mediaId}", v3mux.Handle("/thumbnail/{serverName}/{mediaId}",
makeDownloadAPI("thumbnail", &cfg.MediaAPI, rateLimits, db, client, activeRemoteRequests, activeThumbnailGeneration), makeDownloadAPI("thumbnail_unauthed", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, false),
).Methods(http.MethodGet, http.MethodOptions) ).Methods(http.MethodGet, http.MethodOptions)
// v1 client endpoints requiring auth
downloadHandlerAuthed := httputil.MakeHTMLAPI("download_authed_client", userAPI, cfg.Global.Metrics.Enabled, downloadHandler, httputil.WithAuth())
v1mux.Handle("/config", configHandler).Methods(http.MethodGet, http.MethodOptions)
v1mux.Handle("/download/{serverName}/{mediaId}", downloadHandlerAuthed).Methods(http.MethodGet, http.MethodOptions)
v1mux.Handle("/download/{serverName}/{mediaId}/{downloadName}", downloadHandlerAuthed).Methods(http.MethodGet, http.MethodOptions)
v1mux.Handle("/thumbnail/{serverName}/{mediaId}",
httputil.MakeHTMLAPI("thumbnail", userAPI, cfg.Global.Metrics.Enabled, makeDownloadAPI("thumbnail_authed_client", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, false), httputil.WithAuth()),
).Methods(http.MethodGet, http.MethodOptions)
// same, but for federation
v1fedMux.Handle("/download/{mediaId}", routing.MakeFedAPIHTML(cfg.Global.ServerName, cfg.Global.IsLocalServerName, keyRing,
makeDownloadAPI("download_authed_federation", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, true),
)).Methods(http.MethodGet, http.MethodOptions)
v1fedMux.Handle("/thumbnail/{mediaId}", routing.MakeFedAPIHTML(cfg.Global.ServerName, cfg.Global.IsLocalServerName, keyRing,
makeDownloadAPI("thumbnail_authed_federation", &cfg.MediaAPI, rateLimits, db, client, federationClient, activeRemoteRequests, activeThumbnailGeneration, true),
)).Methods(http.MethodGet, http.MethodOptions)
} }
var thumbnailCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: "dendrite",
Subsystem: "mediaapi",
Name: "thumbnail",
Help: "Total number of media_api requests for thumbnails",
},
[]string{"code", "type"},
)
var thumbnailSize = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "dendrite",
Subsystem: "mediaapi",
Name: "thumbnail_size_bytes",
Help: "Total number of media_api requests for thumbnails",
Buckets: []float64{50, 100, 200, 500, 900, 1500, 3000, 6000},
},
[]string{"code", "type"},
)
var downloadCounter = promauto.NewCounterVec(
prometheus.CounterOpts{
Namespace: "dendrite",
Subsystem: "mediaapi",
Name: "download",
Help: "Total number of media_api requests for full downloads",
},
[]string{"code", "type"},
)
var downloadSize = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "dendrite",
Subsystem: "mediaapi",
Name: "download_size_bytes",
Help: "Total number of media_api requests for full downloads",
Buckets: []float64{200, 500, 900, 1500, 3000, 6000, 10_000, 50_000, 100_000},
},
[]string{"code", "type"},
)
func makeDownloadAPI( func makeDownloadAPI(
name string, name string,
cfg *config.MediaAPI, cfg *config.MediaAPI,
rateLimits *httputil.RateLimits, rateLimits *httputil.RateLimits,
db storage.Database, db storage.Database,
client *fclient.Client, client *fclient.Client,
fedClient fclient.FederationClient,
activeRemoteRequests *types.ActiveRemoteRequests, activeRemoteRequests *types.ActiveRemoteRequests,
activeThumbnailGeneration *types.ActiveThumbnailGeneration, activeThumbnailGeneration *types.ActiveThumbnailGeneration,
forFederation bool,
) http.HandlerFunc { ) http.HandlerFunc {
var counterVec *prometheus.CounterVec var counterVec *prometheus.CounterVec
var sizeVec *prometheus.HistogramVec
var requestType string
if cfg.Matrix.Metrics.Enabled { if cfg.Matrix.Metrics.Enabled {
counterVec = promauto.NewCounterVec( split := strings.Split(name, "_")
prometheus.CounterOpts{ name = split[0]
Name: name, requestType = strings.Join(split[1:], "_")
Help: "Total number of media_api requests for either thumbnails or full downloads",
}, counterVec = thumbnailCounter
[]string{"code"}, sizeVec = thumbnailSize
) if name != "thumbnail" {
counterVec = downloadCounter
sizeVec = downloadSize
}
} }
httpHandler := func(w http.ResponseWriter, req *http.Request) { httpHandler := func(w http.ResponseWriter, req *http.Request) {
req = util.RequestWithLogging(req) req = util.RequestWithLogging(req)
@ -164,16 +237,21 @@ func makeDownloadAPI(
cfg, cfg,
db, db,
client, client,
fedClient,
activeRemoteRequests, activeRemoteRequests,
activeThumbnailGeneration, activeThumbnailGeneration,
name == "thumbnail", strings.HasPrefix(name, "thumbnail"),
vars["downloadName"], vars["downloadName"],
forFederation,
) )
} }
var handlerFunc http.HandlerFunc var handlerFunc http.HandlerFunc
if counterVec != nil { if counterVec != nil {
counterVec = counterVec.MustCurryWith(prometheus.Labels{"type": requestType})
sizeVec2 := sizeVec.MustCurryWith(prometheus.Labels{"type": requestType})
handlerFunc = promhttp.InstrumentHandlerCounter(counterVec, http.HandlerFunc(httpHandler)) handlerFunc = promhttp.InstrumentHandlerCounter(counterVec, http.HandlerFunc(httpHandler))
handlerFunc = promhttp.InstrumentHandlerResponseSize(sizeVec2, handlerFunc).ServeHTTP
} else { } else {
handlerFunc = http.HandlerFunc(httpHandler) handlerFunc = http.HandlerFunc(httpHandler)
} }

View file

@ -78,7 +78,7 @@ func (m *Monolith) AddAllPublicRoutes(
federationapi.AddPublicRoutes( federationapi.AddPublicRoutes(
processCtx, routers, cfg, natsInstance, m.UserAPI, m.FedClient, m.KeyRing, m.RoomserverAPI, m.FederationAPI, enableMetrics, processCtx, routers, cfg, natsInstance, m.UserAPI, m.FedClient, m.KeyRing, m.RoomserverAPI, m.FederationAPI, enableMetrics,
) )
mediaapi.AddPublicRoutes(routers.Media, cm, cfg, m.UserAPI, m.Client) mediaapi.AddPublicRoutes(routers, cm, cfg, m.UserAPI, m.Client, m.FedClient, m.KeyRing)
syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, m.UserAPI, m.RoomserverAPI, caches, enableMetrics) syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, m.UserAPI, m.RoomserverAPI, caches, enableMetrics)
if m.RelayAPI != nil { if m.RelayAPI != nil {