From ec59b618e8ec13ee5183a500ff59e1947d016fb1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 9 Aug 2024 13:20:55 -0600 Subject: [PATCH] Experiment with JWT-based worker redirection --- api/_responses/redirect.go | 86 +++++++++++++++++++++++++++++++++++++- api/custom/byid.go | 46 ++++++++++++++++++++ api/r0/download.go | 2 +- api/r0/thumbnail.go | 2 +- api/routes.go | 2 + go.mod | 9 ++-- go.sum | 14 ++++--- 7 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 api/custom/byid.go diff --git a/api/_responses/redirect.go b/api/_responses/redirect.go index 1e8c66c4..763f0edf 100644 --- a/api/_responses/redirect.go +++ b/api/_responses/redirect.go @@ -1,9 +1,91 @@ package _responses +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "net/url" + "os" + "time" + + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" + "github.com/t2bot/matrix-media-repo/api/_apimeta" + "github.com/t2bot/matrix-media-repo/common/rcontext" +) + +// TODO: Persist and store key (or use some other source of information) +var jwtKey, keyErr = rsa.GenerateKey(rand.Reader, 2048) +var jwtSig, jwtErr = jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: []byte("0102030405060708090A0B0C0D0E0F10")}, (&jose.SignerOptions{}).WithType("JWT")) +var jweEnc, jweErr = jose.NewEncrypter(jose.A128GCM, jose.Recipient{Algorithm: jose.RSA_OAEP, Key: &jwtKey.PublicKey}, nil) + +func init() { + // We don't expect these to happen, so just panic + if keyErr != nil { + panic(keyErr) + } + if jwtErr != nil { + panic(jwtErr) + } + if jweErr != nil { + panic(jweErr) + } + + f1, _ := os.Create("./gdpr-data/jwe.rsa") + f2, _ := os.Create("./gdpr-data/jwe.rsa.pub") + defer f1.Close() + defer f2.Close() + keyPem := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(jwtKey), + }) + pubPem := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(&jwtKey.PublicKey), + }) + f1.Write(keyPem) + f2.Write(pubPem) +} + type RedirectResponse struct { ToUrl string } -func Redirect(url string) *RedirectResponse { - return &RedirectResponse{ToUrl: url} +func Redirect(ctx rcontext.RequestContext, toUrl string, auth _apimeta.AuthContext) *RedirectResponse { + if auth.IsAuthenticated() { + // Figure out who we're authenticating as, and add that as JWT claims + claims := jwt.Claims{ + Issuer: ctx.Request.Host, + } + moreClaims := struct { + Admin bool `json:"adm,omitempty"` + AccessToken string `json:"tok,omitempty"` + }{} + if auth.Server.ServerName != "" { + claims.Subject = auth.Server.ServerName + + // The server won't necessarily re-auth itself with the redirect, so we just put an expiration on it instead + claims.Expiry = jwt.NewNumericDate(time.Now().Add(2 * time.Minute)) + } else { + claims.Subject = auth.User.UserId + moreClaims.Admin = auth.User.IsShared + moreClaims.AccessToken = auth.User.AccessToken + } + raw, err := jwt.Encrypted(jweEnc).Claims(claims).Claims(moreClaims).Serialize() + if err != nil { + panic(err) // should never happen if we've done things correctly + } + + // Now add the query string + parsedUrl, err := url.Parse(toUrl) + if err != nil { + panic(err) // it wouldn't have worked anyways + } + qs := parsedUrl.Query() + qs.Set("org.matrix.media_auth", raw) + parsedUrl.RawQuery = qs.Encode() + toUrl = parsedUrl.String() + } + return &RedirectResponse{ToUrl: toUrl} } diff --git a/api/custom/byid.go b/api/custom/byid.go new file mode 100644 index 00000000..8b64f25f --- /dev/null +++ b/api/custom/byid.go @@ -0,0 +1,46 @@ +package custom + +import ( + "net/http" + + "github.com/t2bot/matrix-media-repo/api/_apimeta" + "github.com/t2bot/matrix-media-repo/api/_responses" + "github.com/t2bot/matrix-media-repo/api/_routers" + "github.com/t2bot/matrix-media-repo/common/rcontext" + "github.com/t2bot/matrix-media-repo/database" + "github.com/t2bot/matrix-media-repo/datastores" + "github.com/t2bot/matrix-media-repo/pipelines/_steps/download" +) + +func GetMediaById(r *http.Request, rctx rcontext.RequestContext, user _apimeta.UserInfo) interface{} { + if !user.IsShared { + return _responses.AuthFailed() + } + + // TODO: This is beyond dangerous and needs proper filtering + + db := database.GetInstance().Media.Prepare(rctx) + ds, err := datastores.Pick(rctx, datastores.LocalMediaKind) + if err != nil { + panic(err) + } + objectId := _routers.GetParam("objectId", r) + medias, err := db.GetByLocation(ds.Id, objectId) + if err != nil { + panic(err) + } + + media := medias[0] + stream, err := download.OpenStream(rctx, media.Locatable) + if err != nil { + panic(err) + } + + return &_responses.DownloadResponse{ + ContentType: media.ContentType, + Filename: media.UploadName, + SizeBytes: media.SizeBytes, + Data: stream, + TargetDisposition: "infer", + } +} diff --git a/api/r0/download.go b/api/r0/download.go index dc48a410..72134c52 100644 --- a/api/r0/download.go +++ b/api/r0/download.go @@ -112,7 +112,7 @@ func DownloadMedia(r *http.Request, rctx rcontext.RequestContext, auth _apimeta. } else if errors.Is(err, common.ErrMediaNotYetUploaded) { return _responses.NotYetUploaded() } else if errors.As(err, &redirect) { - return _responses.Redirect(redirect.RedirectUrl) + return _responses.Redirect(rctx, redirect.RedirectUrl, auth) } rctx.Log.Error("Unexpected error locating media: ", err) sentry.CaptureException(err) diff --git a/api/r0/thumbnail.go b/api/r0/thumbnail.go index 4fff646b..1eb57489 100644 --- a/api/r0/thumbnail.go +++ b/api/r0/thumbnail.go @@ -184,7 +184,7 @@ func ThumbnailMedia(r *http.Request, rctx rcontext.RequestContext, auth _apimeta } } } else if errors.As(err, &redirect) { - return _responses.Redirect(redirect.RedirectUrl) + return _responses.Redirect(rctx, redirect.RedirectUrl, auth) } rctx.Log.Error("Unexpected error locating media: ", err) sentry.CaptureException(err) diff --git a/api/routes.go b/api/routes.go index ee2d87a7..09608b49 100644 --- a/api/routes.go +++ b/api/routes.go @@ -19,6 +19,7 @@ import ( const PrefixMedia = "/_matrix/media" const PrefixClient = "/_matrix/client" const PrefixFederation = "/_matrix/federation" +const PrefixMMR = "/_mmr" func buildRoutes() http.Handler { counter := &_routers.RequestCounter{} @@ -61,6 +62,7 @@ func buildRoutes() http.Handler { purgeOneRoute := makeRoute(_routers.RequireAccessToken(custom.PurgeIndividualRecord), "purge_individual_media", counter) register([]string{"DELETE"}, PrefixMedia, "download/:server/:mediaId", mxUnstable, router, purgeOneRoute) register([]string{"GET"}, PrefixMedia, "usage", msc4034, router, makeRoute(_routers.RequireAccessToken(unstable.PublicUsage), "usage", counter)) + register([]string{"GET"}, PrefixMMR, "byid/:objectId", mxNoVersion, router, makeRoute(_routers.RequireRepoAdmin(custom.GetMediaById), "byid", counter)) // Custom and top-level features router.Handler("GET", fmt.Sprintf("%s/version", PrefixMedia), makeRoute(_routers.OptionalAccessToken(custom.GetVersion), "get_version", counter)) diff --git a/go.mod b/go.mod index 7bd40788..34401c80 100644 --- a/go.mod +++ b/go.mod @@ -38,14 +38,16 @@ require ( github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a github.com/sirupsen/logrus v1.9.3 - golang.org/x/crypto v0.23.0 // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/image v0.18.0 golang.org/x/net v0.25.0 ) require ( github.com/didip/tollbooth/v7 v7.0.1 + github.com/docker/docker v25.0.5+incompatible github.com/docker/go-connections v0.5.0 + github.com/go-jose/go-jose/v4 v4.0.4 github.com/go-redsync/redsync/v4 v4.12.1 github.com/julienschmidt/httprouter v1.3.0 github.com/minio/minio-go/v7 v7.0.69 @@ -62,7 +64,7 @@ require ( github.com/testcontainers/testcontainers-go v0.26.0 github.com/testcontainers/testcontainers-go/modules/postgres v0.26.0 golang.org/x/sync v0.7.0 - golang.org/x/term v0.20.0 + golang.org/x/term v0.22.0 ) require ( @@ -81,7 +83,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.5.0 // indirect - github.com/docker/docker v25.0.5+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect github.com/fatih/color v1.16.0 // indirect @@ -139,7 +140,7 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect diff --git a/go.sum b/go.sum index 70eeaa4d..caea3781 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -415,8 +417,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw= golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= @@ -498,16 +500,16 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=