package clientapi import ( "net/http" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/mediaapi" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // The media endpoints. Uploads keep their Matrix 1.0 paths under // /_matrix/media; downloads and thumbnails are the authenticated // endpoints of Matrix 1.11 under /_matrix/client/v1/media. The // unauthenticated downloads they replaced are only served when // media.legacy_unauthenticated_downloads is on. const ( mediaV1 = "/_matrix/media/v1" mediaV3 = "/_matrix/media/v3" ) // registerMediaRoutes adds the media endpoints, if there is a media // repository. func (c *ClientAPI) registerMediaRoutes(r *httputil.Router) { if c.media == nil { return } // Uploads take the default bucket: they are rare, and each one is // up to media.max_upload_bytes of disk. c.authed(r, "POST "+mediaV3+"/upload", c.limitDefault, c.handleUpload) c.authed(r, "POST "+mediaV1+"/create", c.limitDefault, c.handleCreateMedia) c.authed(r, "PUT "+mediaV3+"/upload/{serverName}/{mediaID}", c.limitDefault, c.handleUploadTo) c.authed(r, "GET "+v1+"/media/config", c.limitMedia, c.handleMediaConfig) c.authed(r, "GET "+mediaV3+"/config", c.limitMedia, c.handleMediaConfig) c.authed(r, "GET "+v1+"/media/download/{serverName}/{mediaID}", c.limitMedia, c.handleDownload) c.authed(r, "GET "+v1+"/media/download/{serverName}/{mediaID}/{fileName}", c.limitMedia, c.handleDownload) c.authed(r, "GET "+v1+"/media/thumbnail/{serverName}/{mediaID}", c.limitMedia, c.handleThumbnail) if c.legacyMedia { c.public(r, "GET "+mediaV3+"/download/{serverName}/{mediaID}", c.limitMedia, c.handleLegacyDownload) c.public(r, "GET "+mediaV3+"/download/{serverName}/{mediaID}/{fileName}", c.limitMedia, c.handleLegacyDownload) c.public(r, "GET "+mediaV3+"/thumbnail/{serverName}/{mediaID}", c.limitMedia, c.handleLegacyThumbnail) } } type uploadResponse struct { ContentURI string `json:"content_uri"` } // handleUpload implements POST /_matrix/media/v3/upload. func (c *ClientAPI) handleUpload(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { if err := c.checkUploadSize(r); err != nil { return err } mxc, err := c.media.Upload(r.Context(), upload(r, auth)) if err != nil { return mediaapi.HTTPError(err) } httputil.WriteJSON(w, http.StatusOK, uploadResponse{ContentURI: mxc}) return nil } type createMediaResponse struct { ContentURI string `json:"content_uri"` UnusedExpiresAt int64 `json:"unused_expires_at"` } // handleCreateMedia implements POST /_matrix/media/v1/create: an mxc:// // URI now, its content later. A client uses it to put a message with an // attachment in the timeline before the attachment has finished // uploading. func (c *ClientAPI) handleCreateMedia(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { mxc, expires, err := c.media.Create(r.Context(), auth.UserID) if err != nil { return mediaapi.HTTPError(err) } httputil.WriteJSON(w, http.StatusOK, createMediaResponse{ContentURI: mxc, UnusedExpiresAt: expires}) return nil } // handleUploadTo implements PUT /_matrix/media/v3/upload/{serverName}/{mediaId}. func (c *ClientAPI) handleUploadTo(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { if err := c.checkUploadSize(r); err != nil { return err } err := c.media.UploadTo(r.Context(), r.PathValue("serverName"), r.PathValue("mediaID"), upload(r, auth)) if err != nil { return mediaapi.HTTPError(err) } httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil } // checkUploadSize refuses an upload that says up front it is too large, // before a byte of it is read. One that does not say is cut off by the // blob store at the same limit. func (c *ClientAPI) checkUploadSize(r *http.Request) error { if r.ContentLength > c.media.MaxUploadBytes() { return mediaapi.HTTPError(mediaapi.ErrTooLarge) } return nil } func upload(r *http.Request, auth userapi.Authenticated) mediaapi.Upload { return mediaapi.Upload{ UserID: auth.UserID, ContentType: r.Header.Get("Content-Type"), FileName: r.URL.Query().Get("filename"), Body: r.Body, } } type mediaConfigResponse struct { UploadSize int64 `json:"m.upload.size"` } // handleMediaConfig implements GET /_matrix/client/v1/media/config and its // deprecated twin under /_matrix/media/v3. func (c *ClientAPI) handleMediaConfig(w http.ResponseWriter, _ *http.Request, _ userapi.Authenticated) error { httputil.WriteJSON(w, http.StatusOK, mediaConfigResponse{UploadSize: c.media.MaxUploadBytes()}) return nil } // handleDownload implements GET /_matrix/client/v1/media/download/... func (c *ClientAPI) handleDownload(w http.ResponseWriter, r *http.Request, _ userapi.Authenticated) error { return c.serveDownload(w, r, false) } // handleThumbnail implements GET /_matrix/client/v1/media/thumbnail/... func (c *ClientAPI) handleThumbnail(w http.ResponseWriter, r *http.Request, _ userapi.Authenticated) error { return c.serveThumbnail(w, r, false) } // handleLegacyDownload implements the deprecated, unauthenticated // GET /_matrix/media/v3/download/..., honouring its allow_remote=false. func (c *ClientAPI) handleLegacyDownload(w http.ResponseWriter, r *http.Request) error { return c.serveDownload(w, r, r.URL.Query().Get("allow_remote") == "false") } // handleLegacyThumbnail implements the deprecated, unauthenticated // GET /_matrix/media/v3/thumbnail/.... func (c *ClientAPI) handleLegacyThumbnail(w http.ResponseWriter, r *http.Request) error { return c.serveThumbnail(w, r, r.URL.Query().Get("allow_remote") == "false") } func (c *ClientAPI) serveDownload(w http.ResponseWriter, r *http.Request, localOnly bool) error { wait, err := mediaapi.WaitParam(r) if err != nil { return err } content, err := c.media.Download(r.Context(), r.PathValue("serverName"), r.PathValue("mediaID"), mediaapi.FetchOptions{Wait: wait, LocalOnly: localOnly}) if err != nil { return mediaapi.HTTPError(err) } defer func() { _ = content.Body.Close() }() // A file name in the path overrides the one it was uploaded with: // that is what the path segment is for. name := content.FileName if override := r.PathValue("fileName"); override != "" { name = override } mediaapi.ServeContent(w, r, content, name) return nil } func (c *ClientAPI) serveThumbnail(w http.ResponseWriter, r *http.Request, localOnly bool) error { req, err := mediaapi.ThumbnailParams(r) if err != nil { return err } wait, err := mediaapi.WaitParam(r) if err != nil { return err } content, err := c.media.Thumbnail(r.Context(), r.PathValue("serverName"), r.PathValue("mediaID"), req, mediaapi.FetchOptions{Wait: wait, LocalOnly: localOnly}) if err != nil { return mediaapi.HTTPError(err) } defer func() { _ = content.Body.Close() }() mediaapi.ServeContent(w, r, content, mediaapi.ThumbnailFileName(content.ContentType)) return nil }