Compare commits
8 commits
main
...
s7evink/au
Author | SHA1 | Date | |
---|---|---|---|
e8b69fba03 | |||
53e545ddc5 | |||
51f6dcbdc5 | |||
aeee46bb71 | |||
89d64c6dca | |||
29ee5401ee | |||
ccd337e1d4 | |||
40eb7cbeef |
|
@ -1,59 +0,0 @@
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
GHCR_NAMESPACE: sigb.us
|
||||
PLATFORMS: linux/amd64
|
||||
FORGEJO_USER: signaryk
|
||||
|
||||
jobs:
|
||||
monolith:
|
||||
name: Monolith image
|
||||
runs-on: docker
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Get release tag & build flags
|
||||
if: github.event_name == 'release' # Only for GitHub releases
|
||||
run: |
|
||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to sigb.us container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.sigb.us
|
||||
username: ${{ env.FORGEJO_USER }}
|
||||
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
||||
- name: Build main monolith image
|
||||
id: docker_build_monolith
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
push: true
|
||||
tags: |
|
||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ github.ref_name }}
|
||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:devel
|
||||
|
||||
- name: Build release monolith image
|
||||
if: github.event_name == 'release' # Only for GitHub releases
|
||||
id: docker_build_monolith_release
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
push: true
|
||||
tags: |
|
||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:stable
|
||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ env.RELEASE_VERSION }}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,7 +5,6 @@
|
|||
|
||||
# Allow GitHub config
|
||||
!.github
|
||||
!.forgejo
|
||||
|
||||
# Downloads
|
||||
/.downloads
|
||||
|
|
|
@ -61,7 +61,7 @@ func LoginFromJSONReader(
|
|||
switch header.Type {
|
||||
case authtypes.LoginTypePassword:
|
||||
typ = &LoginTypePassword{
|
||||
UserAPI: useraccountAPI,
|
||||
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
||||
Config: cfg,
|
||||
}
|
||||
case authtypes.LoginTypeToken:
|
||||
|
|
|
@ -292,14 +292,6 @@ func (ua *fakeUserInternalAPI) QueryAccountByPassword(ctx context.Context, req *
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ua *fakeUserInternalAPI) QueryAccountByLocalpart(ctx context.Context, req *uapi.QueryAccountByLocalpartRequest, res *uapi.QueryAccountByLocalpartResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ua *fakeUserInternalAPI) PerformAccountCreation(ctx context.Context, req *uapi.PerformAccountCreationRequest, res *uapi.PerformAccountCreationResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
||||
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
||||
return nil
|
||||
|
|
|
@ -16,9 +16,6 @@ package auth
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/google/uuid"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -31,6 +28,8 @@ import (
|
|||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
type GetAccountByPassword func(ctx context.Context, req *api.QueryAccountByPasswordRequest, res *api.QueryAccountByPasswordResponse) error
|
||||
|
||||
type PasswordRequest struct {
|
||||
Login
|
||||
Password string `json:"password"`
|
||||
|
@ -38,8 +37,8 @@ type PasswordRequest struct {
|
|||
|
||||
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
||||
type LoginTypePassword struct {
|
||||
GetAccountByPassword GetAccountByPassword
|
||||
Config *config.ClientAPI
|
||||
UserAPI api.UserLoginAPI
|
||||
}
|
||||
|
||||
func (t *LoginTypePassword) Name() string {
|
||||
|
@ -60,21 +59,22 @@ func (t *LoginTypePassword) LoginFromJSON(ctx context.Context, reqBytes []byte)
|
|||
return login, func(context.Context, *util.JSONResponse) {}, nil
|
||||
}
|
||||
|
||||
func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest) (*Login, *util.JSONResponse) {
|
||||
fullUsername := request.Username()
|
||||
if fullUsername == "" {
|
||||
func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) {
|
||||
r := req.(*PasswordRequest)
|
||||
username := r.Username()
|
||||
if username == "" {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.BadJSON("A username must be supplied."),
|
||||
}
|
||||
}
|
||||
if len(request.Password) == 0 {
|
||||
if len(r.Password) == 0 {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.BadJSON("A password must be supplied."),
|
||||
}
|
||||
}
|
||||
username, domain, err := userutil.ParseUsernameParam(fullUsername, t.Config.Matrix)
|
||||
localpart, domain, err := userutil.ParseUsernameParam(username, t.Config.Matrix)
|
||||
if err != nil {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
|
@ -87,38 +87,12 @@ func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest)
|
|||
JSON: spec.InvalidUsername("The server name is not known."),
|
||||
}
|
||||
}
|
||||
|
||||
var account *api.Account
|
||||
if t.Config.Ldap.Enabled {
|
||||
isAdmin, err := t.authenticateLdap(username, request.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc, err := t.getOrCreateAccount(ctx, username, domain, isAdmin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account = acc
|
||||
} else {
|
||||
acc, err := t.authenticateDb(ctx, username, domain, request.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account = acc
|
||||
}
|
||||
|
||||
// Set the user, so login.Username() can do the right thing
|
||||
request.Identifier.User = account.UserID
|
||||
request.User = account.UserID
|
||||
return &request.Login, nil
|
||||
}
|
||||
|
||||
func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string, domain spec.ServerName, password string) (*api.Account, *util.JSONResponse) {
|
||||
// Squash username to all lowercase letters
|
||||
res := &api.QueryAccountByPasswordResponse{}
|
||||
err := t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||
Localpart: strings.ToLower(username),
|
||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||
Localpart: strings.ToLower(localpart),
|
||||
ServerName: domain,
|
||||
PlaintextPassword: password,
|
||||
PlaintextPassword: r.Password,
|
||||
}, res)
|
||||
if err != nil {
|
||||
return nil, &util.JSONResponse{
|
||||
|
@ -127,11 +101,13 @@ func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string,
|
|||
}
|
||||
}
|
||||
|
||||
// If we couldn't find the user by the lower cased localpart, try the provided
|
||||
// localpart as is.
|
||||
if !res.Exists {
|
||||
err = t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||
Localpart: username,
|
||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||
Localpart: localpart,
|
||||
ServerName: domain,
|
||||
PlaintextPassword: password,
|
||||
PlaintextPassword: r.Password,
|
||||
}, res)
|
||||
if err != nil {
|
||||
return nil, &util.JSONResponse{
|
||||
|
@ -139,6 +115,8 @@ func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string,
|
|||
JSON: spec.Unknown("Unable to fetch account by password."),
|
||||
}
|
||||
}
|
||||
// Technically we could tell them if the user does not exist by checking if err == sql.ErrNoRows
|
||||
// but that would leak the existence of the user.
|
||||
if !res.Exists {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusForbidden,
|
||||
|
@ -146,141 +124,8 @@ func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string,
|
|||
}
|
||||
}
|
||||
}
|
||||
return res.Account, nil
|
||||
}
|
||||
func (t *LoginTypePassword) authenticateLdap(username, password string) (bool, *util.JSONResponse) {
|
||||
var conn *ldap.Conn
|
||||
conn, err := ldap.DialURL(t.Config.Ldap.Uri)
|
||||
if err != nil {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.Unknown("unable to connect to ldap: " + err.Error()),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if t.Config.Ldap.AdminBindEnabled {
|
||||
err = conn.Bind(t.Config.Ldap.AdminBindDn, t.Config.Ldap.AdminBindPassword)
|
||||
if err != nil {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.Unknown("unable to bind to ldap: " + err.Error()),
|
||||
}
|
||||
}
|
||||
filter := strings.ReplaceAll(t.Config.Ldap.SearchFilter, "{username}", username)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
t.Config.Ldap.BaseDn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||
0, 0, false, filter, []string{t.Config.Ldap.SearchAttribute}, nil,
|
||||
)
|
||||
result, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.Unknown("unable to bind to search ldap: " + err.Error()),
|
||||
}
|
||||
}
|
||||
if len(result.Entries) > 1 {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.BadJSON("'user' must be duplicated."),
|
||||
}
|
||||
}
|
||||
if len(result.Entries) < 1 {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.BadJSON("'user' not found."),
|
||||
}
|
||||
}
|
||||
|
||||
userDN := result.Entries[0].DN
|
||||
err = conn.Bind(userDN, password)
|
||||
if err != nil {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.InvalidUsername(err.Error()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bindDn := strings.ReplaceAll(t.Config.Ldap.UserBindDn, "{username}", username)
|
||||
err = conn.Bind(bindDn, password)
|
||||
if err != nil {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.InvalidUsername(err.Error()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isAdmin, err := t.isLdapAdmin(conn, username)
|
||||
if err != nil {
|
||||
return false, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.InvalidUsername(err.Error()),
|
||||
}
|
||||
}
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
func (t *LoginTypePassword) isLdapAdmin(conn *ldap.Conn, username string) (bool, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
t.Config.Ldap.AdminGroupDn,
|
||||
ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
|
||||
strings.ReplaceAll(t.Config.Ldap.AdminGroupFilter, "{username}", username),
|
||||
[]string{t.Config.Ldap.AdminGroupAttribute},
|
||||
nil)
|
||||
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(sr.Entries) < 1 {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (t *LoginTypePassword) getOrCreateAccount(ctx context.Context, username string, domain spec.ServerName, admin bool) (*api.Account, *util.JSONResponse) {
|
||||
var existing api.QueryAccountByLocalpartResponse
|
||||
err := t.UserAPI.QueryAccountByLocalpart(ctx, &api.QueryAccountByLocalpartRequest{
|
||||
Localpart: username,
|
||||
ServerName: domain,
|
||||
}, &existing)
|
||||
|
||||
if err == nil {
|
||||
return existing.Account, nil
|
||||
}
|
||||
if err != sql.ErrNoRows {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.InvalidUsername(err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
accountType := api.AccountTypeUser
|
||||
if admin {
|
||||
accountType = api.AccountTypeAdmin
|
||||
}
|
||||
var created api.PerformAccountCreationResponse
|
||||
err = t.UserAPI.PerformAccountCreation(ctx, &api.PerformAccountCreationRequest{
|
||||
AppServiceID: "ldap",
|
||||
Localpart: username,
|
||||
Password: uuid.New().String(),
|
||||
AccountType: accountType,
|
||||
OnConflict: api.ConflictAbort,
|
||||
}, &created)
|
||||
|
||||
if err != nil {
|
||||
if _, ok := err.(*api.ErrorConflict); ok {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.UserInUse("Desired user ID is already taken."),
|
||||
}
|
||||
}
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
||||
}
|
||||
}
|
||||
return created.Account, nil
|
||||
// Set the user, so login.Username() can do the right thing
|
||||
r.Identifier.User = res.Account.UserID
|
||||
r.User = res.Account.UserID
|
||||
return &r.Login, nil
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ type UserInteractive struct {
|
|||
|
||||
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
||||
typePassword := &LoginTypePassword{
|
||||
UserAPI: userAccountAPI,
|
||||
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
||||
Config: cfg,
|
||||
}
|
||||
return &UserInteractive{
|
||||
|
|
|
@ -45,14 +45,6 @@ func (d *fakeAccountDatabase) QueryAccountByPassword(ctx context.Context, req *a
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *fakeAccountDatabase) QueryAccountByLocalpart(ctx context.Context, req *api.QueryAccountByLocalpartRequest, res *api.QueryAccountByLocalpartResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *fakeAccountDatabase) PerformAccountCreation(ctx context.Context, req *api.PerformAccountCreationRequest, res *api.PerformAccountCreationResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setup() *UserInteractive {
|
||||
cfg := &config.ClientAPI{
|
||||
Matrix: &config.Global{
|
||||
|
|
|
@ -32,7 +32,7 @@ type crossSigningRequest struct {
|
|||
}
|
||||
|
||||
func UploadCrossSigningDeviceKeys(
|
||||
req *http.Request,
|
||||
req *http.Request, userInteractiveAuth *auth.UserInteractive,
|
||||
keyserverAPI api.ClientKeyAPI, device *api.Device,
|
||||
accountAPI api.ClientUserAPI, cfg *config.ClientAPI,
|
||||
) util.JSONResponse {
|
||||
|
@ -62,7 +62,7 @@ func UploadCrossSigningDeviceKeys(
|
|||
}
|
||||
}
|
||||
typePassword := auth.LoginTypePassword{
|
||||
UserAPI: accountAPI,
|
||||
GetAccountByPassword: accountAPI.QueryAccountByPassword,
|
||||
Config: cfg,
|
||||
}
|
||||
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
||||
|
|
|
@ -73,7 +73,7 @@ func Password(
|
|||
|
||||
// Check if the existing password is correct.
|
||||
typePassword := auth.LoginTypePassword{
|
||||
UserAPI: userAPI,
|
||||
GetAccountByPassword: userAPI.QueryAccountByPassword,
|
||||
Config: cfg,
|
||||
}
|
||||
if _, authErr := typePassword.Login(req.Context(), &r.Auth.PasswordRequest); authErr != nil {
|
||||
|
|
|
@ -94,6 +94,7 @@ func Setup(
|
|||
unstableFeatures := map[string]bool{
|
||||
"org.matrix.e2e_cross_signing": true,
|
||||
"org.matrix.msc2285.stable": true,
|
||||
"org.matrix.msc3916": true,
|
||||
}
|
||||
for _, msc := range cfg.MSCs.MSCs {
|
||||
unstableFeatures["org.matrix."+msc] = true
|
||||
|
@ -732,7 +733,7 @@ func Setup(
|
|||
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
||||
|
||||
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)
|
||||
AuthFallback(w, req, vars["authType"], cfg)
|
||||
}),
|
||||
|
@ -1448,7 +1449,7 @@ func Setup(
|
|||
// Cross-signing device keys
|
||||
|
||||
postDeviceSigningKeys := httputil.MakeAuthAPI("post_device_signing_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||
return UploadCrossSigningDeviceKeys(req, userAPI, device, userAPI, cfg)
|
||||
return UploadCrossSigningDeviceKeys(req, userInteractiveAuth, userAPI, device, userAPI, cfg)
|
||||
})
|
||||
|
||||
postDeviceSigningSignatures := httputil.MakeAuthAPI("post_device_signing_signatures", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||
|
|
|
@ -16,6 +16,7 @@ package routing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
@ -678,6 +679,53 @@ func MakeFedAPI(
|
|||
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 {
|
||||
FsAPI *fedInternal.FederationInternalAPI
|
||||
origins sync.Map
|
||||
|
|
5
go.mod
5
go.mod
|
@ -12,7 +12,6 @@ require (
|
|||
github.com/docker/docker v24.0.9+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/getsentry/sentry-go v0.14.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.4
|
||||
github.com/gologme/log v1.3.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/uuid v1.3.0
|
||||
|
@ -23,7 +22,7 @@ require (
|
|||
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/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/util v0.0.0-20221111132719-399730281e66
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
|
@ -58,7 +57,6 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
|
||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/RoaringBitmap/roaring v1.2.3 // indirect
|
||||
|
@ -86,7 +84,6 @@ require (
|
|||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
|
||||
|
|
13
go.sum
13
go.sum
|
@ -5,8 +5,6 @@ github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3
|
|||
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e h1:NeAW1fUYUEWhft7pkxDf6WoUvEZJ/uOKsvtpjLnn8MU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
|
@ -117,12 +115,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
|
|||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
|
@ -217,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/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/gomatrixserverlib v0.0.0-20240328203753-c2391f7113a5 h1:GuxmpyjZQoqb6UFQgKq8Td3wIITlXln/sItqp1jbTTA=
|
||||
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 h1:drDloOznSn5b9vhrsK5LNziiPECZDvgqwYim6s4hmqU=
|
||||
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/go.mod h1:ReWMS/LoVnOiRAdq9sNUC2NZnd1mZkMNB52QhpTRWjg=
|
||||
github.com/matrix-org/util v0.0.0-20221111132719-399730281e66 h1:6z4KxomXSIGWqhHcfzExgkH3Z3UkIXry4ibJS4Aqz2Y=
|
||||
|
@ -389,7 +383,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
|
@ -413,7 +406,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -435,7 +427,6 @@ golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -44,6 +45,7 @@ type BasicAuth struct {
|
|||
|
||||
type AuthAPIOpts struct {
|
||||
GuestAccessAllowed bool
|
||||
WithAuth bool
|
||||
}
|
||||
|
||||
// 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.
|
||||
func MakeAuthAPI(
|
||||
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
|
||||
// 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) {
|
||||
trace, ctx := internal.StartTask(req.Context(), metricsName)
|
||||
defer trace.EndTask()
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,23 +15,26 @@
|
|||
package mediaapi
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/matrix-org/dendrite/internal/httputil"
|
||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||
"github.com/matrix-org/dendrite/mediaapi/routing"
|
||||
"github.com/matrix-org/dendrite/mediaapi/storage"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component.
|
||||
func AddPublicRoutes(
|
||||
mediaRouter *mux.Router,
|
||||
routers httputil.Routers,
|
||||
cm *sqlutil.Connections,
|
||||
cfg *config.Dendrite,
|
||||
userAPI userapi.MediaUserAPI,
|
||||
client *fclient.Client,
|
||||
fedClient fclient.FederationClient,
|
||||
keyRing gomatrixserverlib.JSONVerifier,
|
||||
) {
|
||||
mediaDB, err := storage.NewMediaAPIDatasource(cm, &cfg.MediaAPI.Database)
|
||||
if err != nil {
|
||||
|
@ -39,6 +42,6 @@ func AddPublicRoutes(
|
|||
}
|
||||
|
||||
routing.Setup(
|
||||
mediaRouter, cfg, mediaDB, userAPI, client,
|
||||
routers, cfg, mediaDB, userAPI, client, fedClient, keyRing,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ import (
|
|||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -31,6 +33,7 @@ import (
|
|||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/matrix-org/dendrite/mediaapi/fileutils"
|
||||
"github.com/matrix-org/dendrite/mediaapi/storage"
|
||||
"github.com/matrix-org/dendrite/mediaapi/thumbnailer"
|
||||
|
@ -61,6 +64,9 @@ type downloadRequest struct {
|
|||
ThumbnailSize types.ThumbnailSize
|
||||
Logger *log.Entry
|
||||
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
|
||||
|
@ -111,11 +117,17 @@ func Download(
|
|||
cfg *config.MediaAPI,
|
||||
db storage.Database,
|
||||
client *fclient.Client,
|
||||
fedClient fclient.FederationClient,
|
||||
activeRemoteRequests *types.ActiveRemoteRequests,
|
||||
activeThumbnailGeneration *types.ActiveThumbnailGeneration,
|
||||
isThumbnailRequest bool,
|
||||
customFilename string,
|
||||
forFederation bool,
|
||||
) {
|
||||
// This happens if we call Download for a federation request
|
||||
if forFederation && origin == "" {
|
||||
origin = cfg.Matrix.ServerName
|
||||
}
|
||||
dReq := &downloadRequest{
|
||||
MediaMetadata: &types.MediaMetadata{
|
||||
MediaID: mediaID,
|
||||
|
@ -127,6 +139,9 @@ func Download(
|
|||
"MediaID": mediaID,
|
||||
}),
|
||||
DownloadFilename: customFilename,
|
||||
forFederation: forFederation,
|
||||
origin: cfg.Matrix.ServerName,
|
||||
fedClient: fedClient,
|
||||
}
|
||||
|
||||
if dReq.IsThumbnailRequest {
|
||||
|
@ -355,7 +370,7 @@ func (r *downloadRequest) respondFromLocalFile(
|
|||
}).Trace("Responding with file")
|
||||
responseFile = file
|
||||
responseMetadata = r.MediaMetadata
|
||||
if err := r.addDownloadFilenameToHeaders(w, responseMetadata); err != nil {
|
||||
if err = r.addDownloadFilenameToHeaders(w, responseMetadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@ -369,9 +384,50 @@ func (r *downloadRequest) respondFromLocalFile(
|
|||
" object-src 'self';"
|
||||
w.Header().Set("Content-Security-Policy", contentSecurityPolicy)
|
||||
|
||||
if _, err := io.Copy(w, responseFile); err != nil {
|
||||
if !r.forFederation {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -722,8 +778,7 @@ func (r *downloadRequest) fetchRemoteFileAndStoreMetadata(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string, body *io.ReadCloser, maxFileSizeBytes config.FileSizeBytes) (int64, io.Reader, error) {
|
||||
reader := *body
|
||||
func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string, reader io.ReadCloser, maxFileSizeBytes config.FileSizeBytes) (int64, io.Reader, error) {
|
||||
var contentLength int64
|
||||
|
||||
if contentLengthHeader != "" {
|
||||
|
@ -742,7 +797,7 @@ func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string,
|
|||
|
||||
// We successfully parsed the Content-Length, so we'll return a limited
|
||||
// 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
|
||||
} else {
|
||||
// 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
|
||||
// to disk.
|
||||
if maxFileSizeBytes > 0 {
|
||||
reader = io.NopCloser(io.LimitReader(*body, int64(maxFileSizeBytes)))
|
||||
reader = io.NopCloser(io.LimitReader(reader, int64(maxFileSizeBytes)))
|
||||
}
|
||||
contentLength = 0
|
||||
}
|
||||
|
@ -759,6 +814,7 @@ func (r *downloadRequest) GetContentLengthAndReader(contentLengthHeader string,
|
|||
return contentLength, reader, nil
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func (r *downloadRequest) fetchRemoteFile(
|
||||
ctx context.Context,
|
||||
client *fclient.Client,
|
||||
|
@ -767,19 +823,69 @@ func (r *downloadRequest) fetchRemoteFile(
|
|||
) (types.Path, bool, error) {
|
||||
r.Logger.Debug("Fetching remote file")
|
||||
|
||||
// Attempt to download via authenticated media endpoint
|
||||
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) {
|
||||
isMultiPart = false
|
||||
// try again on the unauthed endpoint
|
||||
// create request for remote file
|
||||
resp, err := client.CreateMediaDownloadRequest(ctx, r.MediaMetadata.Origin, string(r.MediaMetadata.MediaID))
|
||||
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)
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close() // nolint: errcheck
|
||||
|
||||
// If this wasn't a multipart response, set the Content-Type now. Will be overwritten
|
||||
// by the multipart Content-Type below.
|
||||
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)
|
||||
contentLength, reader, parseErr = r.GetContentLengthAndReader(resp.Header.Get("Content-Length"), resp.Body, maxFileSizeBytes)
|
||||
}
|
||||
|
||||
if parseErr != nil {
|
||||
return "", false, parseErr
|
||||
}
|
||||
|
@ -790,7 +896,6 @@ func (r *downloadRequest) fetchRemoteFile(
|
|||
}
|
||||
|
||||
r.MediaMetadata.FileSizeBytes = types.FileSizeBytes(contentLength)
|
||||
r.MediaMetadata.ContentType = types.ContentType(resp.Header.Get("Content-Type"))
|
||||
|
||||
dispositionHeader := resp.Header.Get("Content-Disposition")
|
||||
if _, params, e := mime.ParseMediaType(dispositionHeader); e == nil {
|
||||
|
|
|
@ -20,11 +20,13 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/matrix-org/dendrite/federationapi/routing"
|
||||
"github.com/matrix-org/dendrite/internal/httputil"
|
||||
"github.com/matrix-org/dendrite/mediaapi/storage"
|
||||
"github.com/matrix-org/dendrite/mediaapi/types"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/matrix-org/util"
|
||||
|
@ -45,15 +47,19 @@ type configResponse struct {
|
|||
// applied:
|
||||
// nolint: gocyclo
|
||||
func Setup(
|
||||
publicAPIMux *mux.Router,
|
||||
routers httputil.Routers,
|
||||
cfg *config.Dendrite,
|
||||
db storage.Database,
|
||||
userAPI userapi.MediaUserAPI,
|
||||
client *fclient.Client,
|
||||
federationClient fclient.FederationClient,
|
||||
keyRing gomatrixserverlib.JSONVerifier,
|
||||
) {
|
||||
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{
|
||||
PathToResult: map[string]*types.ThumbnailGenerationResult{},
|
||||
|
@ -90,33 +96,100 @@ func Setup(
|
|||
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}/{downloadName}", downloadHandler).Methods(http.MethodGet, http.MethodOptions)
|
||||
|
||||
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)
|
||||
|
||||
// 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(
|
||||
name string,
|
||||
cfg *config.MediaAPI,
|
||||
rateLimits *httputil.RateLimits,
|
||||
db storage.Database,
|
||||
client *fclient.Client,
|
||||
fedClient fclient.FederationClient,
|
||||
activeRemoteRequests *types.ActiveRemoteRequests,
|
||||
activeThumbnailGeneration *types.ActiveThumbnailGeneration,
|
||||
forFederation bool,
|
||||
) http.HandlerFunc {
|
||||
var counterVec *prometheus.CounterVec
|
||||
var sizeVec *prometheus.HistogramVec
|
||||
var requestType string
|
||||
if cfg.Matrix.Metrics.Enabled {
|
||||
counterVec = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: name,
|
||||
Help: "Total number of media_api requests for either thumbnails or full downloads",
|
||||
},
|
||||
[]string{"code"},
|
||||
)
|
||||
split := strings.Split(name, "_")
|
||||
name = split[0]
|
||||
requestType = strings.Join(split[1:], "_")
|
||||
|
||||
counterVec = thumbnailCounter
|
||||
sizeVec = thumbnailSize
|
||||
if name != "thumbnail" {
|
||||
counterVec = downloadCounter
|
||||
sizeVec = downloadSize
|
||||
}
|
||||
}
|
||||
httpHandler := func(w http.ResponseWriter, req *http.Request) {
|
||||
req = util.RequestWithLogging(req)
|
||||
|
@ -164,16 +237,21 @@ func makeDownloadAPI(
|
|||
cfg,
|
||||
db,
|
||||
client,
|
||||
fedClient,
|
||||
activeRemoteRequests,
|
||||
activeThumbnailGeneration,
|
||||
name == "thumbnail",
|
||||
strings.HasPrefix(name, "thumbnail"),
|
||||
vars["downloadName"],
|
||||
forFederation,
|
||||
)
|
||||
}
|
||||
|
||||
var handlerFunc http.HandlerFunc
|
||||
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.InstrumentHandlerResponseSize(sizeVec2, handlerFunc).ServeHTTP
|
||||
} else {
|
||||
handlerFunc = http.HandlerFunc(httpHandler)
|
||||
}
|
||||
|
|
|
@ -56,26 +56,9 @@ type ClientAPI struct {
|
|||
RateLimiting RateLimiting `yaml:"rate_limiting"`
|
||||
|
||||
MSCs *MSCs `yaml:"-"`
|
||||
|
||||
Ldap Ldap `yaml:"ldap"`
|
||||
}
|
||||
|
||||
type Ldap struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Uri string `yaml:"uri"`
|
||||
BaseDn string `yaml:"base_dn"`
|
||||
SearchFilter string `yaml:"search_filter"`
|
||||
SearchAttribute string `yaml:"search_attribute"`
|
||||
AdminBindEnabled bool `yaml:"admin_bind_enabled"`
|
||||
AdminBindDn string `yaml:"admin_bind_dn"`
|
||||
AdminBindPassword string `yaml:"admin_bind_password"`
|
||||
UserBindDn string `yaml:"user_bind_dn"`
|
||||
AdminGroupDn string `yaml:"admin_group_dn"`
|
||||
AdminGroupFilter string `yaml:"admin_group_filter"`
|
||||
AdminGroupAttribute string `yaml:"admin_group_attribute"`
|
||||
}
|
||||
|
||||
func (c *ClientAPI) Defaults(_ DefaultOpts) {
|
||||
func (c *ClientAPI) Defaults(opts DefaultOpts) {
|
||||
c.RegistrationSharedSecret = ""
|
||||
c.RegistrationRequiresToken = false
|
||||
c.RecaptchaPublicKey = ""
|
||||
|
|
|
@ -78,7 +78,7 @@ func (m *Monolith) AddAllPublicRoutes(
|
|||
federationapi.AddPublicRoutes(
|
||||
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)
|
||||
|
||||
if m.RelayAPI != nil {
|
||||
|
|
|
@ -144,8 +144,6 @@ type QueryAcccessTokenAPI interface {
|
|||
|
||||
type UserLoginAPI interface {
|
||||
QueryAccountByPassword(ctx context.Context, req *QueryAccountByPasswordRequest, res *QueryAccountByPasswordResponse) error
|
||||
QueryAccountByLocalpart(ctx context.Context, req *QueryAccountByLocalpartRequest, res *QueryAccountByLocalpartResponse) error
|
||||
PerformAccountCreation(ctx context.Context, req *PerformAccountCreationRequest, res *PerformAccountCreationResponse) error
|
||||
}
|
||||
|
||||
type PerformKeyBackupRequest struct {
|
||||
|
|
Loading…
Reference in a new issue