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
|
# Allow GitHub config
|
||||||
!.github
|
!.github
|
||||||
!.forgejo
|
|
||||||
|
|
||||||
# Downloads
|
# Downloads
|
||||||
/.downloads
|
/.downloads
|
||||||
|
|
|
@ -61,8 +61,8 @@ func LoginFromJSONReader(
|
||||||
switch header.Type {
|
switch header.Type {
|
||||||
case authtypes.LoginTypePassword:
|
case authtypes.LoginTypePassword:
|
||||||
typ = &LoginTypePassword{
|
typ = &LoginTypePassword{
|
||||||
UserAPI: useraccountAPI,
|
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
case authtypes.LoginTypeToken:
|
case authtypes.LoginTypeToken:
|
||||||
typ = &LoginTypeToken{
|
typ = &LoginTypeToken{
|
||||||
|
|
|
@ -292,14 +292,6 @@ func (ua *fakeUserInternalAPI) QueryAccountByPassword(ctx context.Context, req *
|
||||||
return nil
|
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 {
|
func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
||||||
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -16,9 +16,6 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -31,6 +28,8 @@ import (
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GetAccountByPassword func(ctx context.Context, req *api.QueryAccountByPasswordRequest, res *api.QueryAccountByPasswordResponse) error
|
||||||
|
|
||||||
type PasswordRequest struct {
|
type PasswordRequest struct {
|
||||||
Login
|
Login
|
||||||
Password string `json:"password"`
|
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
|
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
||||||
type LoginTypePassword struct {
|
type LoginTypePassword struct {
|
||||||
Config *config.ClientAPI
|
GetAccountByPassword GetAccountByPassword
|
||||||
UserAPI api.UserLoginAPI
|
Config *config.ClientAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Name() string {
|
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
|
return login, func(context.Context, *util.JSONResponse) {}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest) (*Login, *util.JSONResponse) {
|
func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) {
|
||||||
fullUsername := request.Username()
|
r := req.(*PasswordRequest)
|
||||||
if fullUsername == "" {
|
username := r.Username()
|
||||||
|
if username == "" {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.BadJSON("A username must be supplied."),
|
JSON: spec.BadJSON("A username must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(request.Password) == 0 {
|
if len(r.Password) == 0 {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.BadJSON("A password must be supplied."),
|
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 {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
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."),
|
JSON: spec.InvalidUsername("The server name is not known."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Squash username to all lowercase letters
|
||||||
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) {
|
|
||||||
res := &api.QueryAccountByPasswordResponse{}
|
res := &api.QueryAccountByPasswordResponse{}
|
||||||
err := t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||||
Localpart: strings.ToLower(username),
|
Localpart: strings.ToLower(localpart),
|
||||||
ServerName: domain,
|
ServerName: domain,
|
||||||
PlaintextPassword: password,
|
PlaintextPassword: r.Password,
|
||||||
}, res)
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
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 {
|
if !res.Exists {
|
||||||
err = t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||||
Localpart: username,
|
Localpart: localpart,
|
||||||
ServerName: domain,
|
ServerName: domain,
|
||||||
PlaintextPassword: password,
|
PlaintextPassword: r.Password,
|
||||||
}, res)
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
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."),
|
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 {
|
if !res.Exists {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
|
@ -146,141 +124,8 @@ func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.Account, nil
|
// Set the user, so login.Username() can do the right thing
|
||||||
}
|
r.Identifier.User = res.Account.UserID
|
||||||
func (t *LoginTypePassword) authenticateLdap(username, password string) (bool, *util.JSONResponse) {
|
r.User = res.Account.UserID
|
||||||
var conn *ldap.Conn
|
return &r.Login, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,8 +113,8 @@ type UserInteractive struct {
|
||||||
|
|
||||||
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
||||||
typePassword := &LoginTypePassword{
|
typePassword := &LoginTypePassword{
|
||||||
UserAPI: userAccountAPI,
|
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
return &UserInteractive{
|
return &UserInteractive{
|
||||||
Flows: []userInteractiveFlow{
|
Flows: []userInteractiveFlow{
|
||||||
|
|
|
@ -45,14 +45,6 @@ func (d *fakeAccountDatabase) QueryAccountByPassword(ctx context.Context, req *a
|
||||||
return nil
|
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 {
|
func setup() *UserInteractive {
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
|
|
|
@ -32,7 +32,7 @@ type crossSigningRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadCrossSigningDeviceKeys(
|
func UploadCrossSigningDeviceKeys(
|
||||||
req *http.Request,
|
req *http.Request, userInteractiveAuth *auth.UserInteractive,
|
||||||
keyserverAPI api.ClientKeyAPI, device *api.Device,
|
keyserverAPI api.ClientKeyAPI, device *api.Device,
|
||||||
accountAPI api.ClientUserAPI, cfg *config.ClientAPI,
|
accountAPI api.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
@ -62,8 +62,8 @@ func UploadCrossSigningDeviceKeys(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
typePassword := auth.LoginTypePassword{
|
typePassword := auth.LoginTypePassword{
|
||||||
UserAPI: accountAPI,
|
GetAccountByPassword: accountAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
|
|
|
@ -73,8 +73,8 @@ func Password(
|
||||||
|
|
||||||
// Check if the existing password is correct.
|
// Check if the existing password is correct.
|
||||||
typePassword := auth.LoginTypePassword{
|
typePassword := auth.LoginTypePassword{
|
||||||
UserAPI: userAPI,
|
GetAccountByPassword: userAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
if _, authErr := typePassword.Login(req.Context(), &r.Auth.PasswordRequest); authErr != nil {
|
if _, authErr := typePassword.Login(req.Context(), &r.Auth.PasswordRequest); authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
|
|
|
@ -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)
|
||||||
}),
|
}),
|
||||||
|
@ -1448,7 +1449,7 @@ func Setup(
|
||||||
// Cross-signing device keys
|
// Cross-signing device keys
|
||||||
|
|
||||||
postDeviceSigningKeys := httputil.MakeAuthAPI("post_device_signing_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
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 {
|
postDeviceSigningSignatures := httputil.MakeAuthAPI("post_device_signing_signatures", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
|
|
@ -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
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -12,7 +12,6 @@ require (
|
||||||
github.com/docker/docker v24.0.9+incompatible
|
github.com/docker/docker v24.0.9+incompatible
|
||||||
github.com/docker/go-connections v0.4.0
|
github.com/docker/go-connections v0.4.0
|
||||||
github.com/getsentry/sentry-go v0.14.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/gologme/log v1.3.0
|
||||||
github.com/google/go-cmp v0.6.0
|
github.com/google/go-cmp v0.6.0
|
||||||
github.com/google/uuid v1.3.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/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
|
||||||
|
@ -58,7 +57,6 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
|
|
||||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
github.com/RoaringBitmap/roaring v1.2.3 // 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/distribution v2.8.2+incompatible // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // 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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // 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/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 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
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/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 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
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/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/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/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-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-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-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/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
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/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=
|
||||||
|
@ -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-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-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-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.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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
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-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-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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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.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.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.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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,26 +56,9 @@ type ClientAPI struct {
|
||||||
RateLimiting RateLimiting `yaml:"rate_limiting"`
|
RateLimiting RateLimiting `yaml:"rate_limiting"`
|
||||||
|
|
||||||
MSCs *MSCs `yaml:"-"`
|
MSCs *MSCs `yaml:"-"`
|
||||||
|
|
||||||
Ldap Ldap `yaml:"ldap"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Ldap struct {
|
func (c *ClientAPI) Defaults(opts DefaultOpts) {
|
||||||
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) {
|
|
||||||
c.RegistrationSharedSecret = ""
|
c.RegistrationSharedSecret = ""
|
||||||
c.RegistrationRequiresToken = false
|
c.RegistrationRequiresToken = false
|
||||||
c.RecaptchaPublicKey = ""
|
c.RecaptchaPublicKey = ""
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -144,8 +144,6 @@ type QueryAcccessTokenAPI interface {
|
||||||
|
|
||||||
type UserLoginAPI interface {
|
type UserLoginAPI interface {
|
||||||
QueryAccountByPassword(ctx context.Context, req *QueryAccountByPasswordRequest, res *QueryAccountByPasswordResponse) error
|
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 {
|
type PerformKeyBackupRequest struct {
|
||||||
|
|
Loading…
Reference in a new issue