forked from mirror/mautrix-discord
Add support for authenticated media
This commit is contained in:
parent
a126a36249
commit
035f2a408b
8 changed files with 99 additions and 16 deletions
|
@ -1,6 +1,14 @@
|
|||
# v0.7.0 (unreleased)
|
||||
|
||||
* Bumped minimum Go version to 1.21.
|
||||
* Added support for Matrix v1.11 authenticated media.
|
||||
* This also changes how avatars are sent to Discord when using relay webhooks.
|
||||
To keep avatars working, you must configure `public_address` in the *bridge*
|
||||
section of the config and proxy `/mautrix-discord/avatar/*` from that
|
||||
address to the bridge.
|
||||
* Added `create-portal` command to create individual portals bypassing the
|
||||
bridging mode. When used in combination with the `if-portal-exists` bridging
|
||||
mode, this can be used to bridge individual channels from a guild.
|
||||
* Changed how direct media access works to make it compatible with Discord's
|
||||
signed URL requirement. The new system must be enabled manually, see
|
||||
[docs](https://docs.mau.fi/bridges/go/discord/direct-media.html) for info.
|
||||
|
|
|
@ -37,6 +37,9 @@ type BridgeConfig struct {
|
|||
|
||||
PortalMessageBuffer int `yaml:"portal_message_buffer"`
|
||||
|
||||
PublicAddress string `yaml:"public_address"`
|
||||
AvatarProxyKey string `yaml:"avatar_proxy_key"`
|
||||
|
||||
DeliveryReceipts bool `yaml:"delivery_receipts"`
|
||||
MessageStatusEvents bool `yaml:"message_status_events"`
|
||||
MessageErrorNotices bool `yaml:"message_error_notices"`
|
||||
|
|
|
@ -26,8 +26,6 @@ import (
|
|||
func DoUpgrade(helper *up.Helper) {
|
||||
bridgeconfig.Upgrader.DoUpgrade(helper)
|
||||
|
||||
helper.Copy(up.Str|up.Null, "homeserver", "public_address")
|
||||
|
||||
helper.Copy(up.Str, "bridge", "username_template")
|
||||
helper.Copy(up.Str, "bridge", "displayname_template")
|
||||
helper.Copy(up.Str, "bridge", "channel_name_template")
|
||||
|
@ -42,6 +40,12 @@ func DoUpgrade(helper *up.Helper) {
|
|||
helper.Copy(up.Str, "bridge", "private_chat_portal_meta")
|
||||
}
|
||||
helper.Copy(up.Int, "bridge", "startup_private_channel_create_limit")
|
||||
helper.Copy(up.Str|up.Null, "bridge", "public_address")
|
||||
if apkey, ok := helper.Get(up.Str, "bridge", "avatar_proxy_key"); !ok || apkey == "generate" {
|
||||
helper.Set(up.Str, random.String(32), "bridge", "avatar_proxy_key")
|
||||
} else {
|
||||
helper.Copy(up.Str, "bridge", "avatar_proxy_key")
|
||||
}
|
||||
helper.Copy(up.Int, "bridge", "portal_message_buffer")
|
||||
helper.Copy(up.Bool, "bridge", "delivery_receipts")
|
||||
helper.Copy(up.Bool, "bridge", "message_status_events")
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
homeserver:
|
||||
# The address that this appservice can use to connect to the homeserver.
|
||||
address: https://matrix.example.com
|
||||
# Publicly accessible base URL for media, used for avatars in relay mode.
|
||||
# If not set, the connection address above will be used.
|
||||
public_address: null
|
||||
# The domain of the homeserver (also known as server_name, used for MXIDs, etc).
|
||||
domain: example.com
|
||||
|
||||
|
@ -113,6 +110,13 @@ bridge:
|
|||
# If set to `never`, DM rooms will never have names and avatars set.
|
||||
private_chat_portal_meta: default
|
||||
|
||||
# Publicly accessible base URL that Discord can use to reach the bridge, used for avatars in relay mode.
|
||||
# If not set, avatars will not be bridged. Only the /mautrix-discord/avatar/{server}/{id}/{hash} endpoint is used on this address.
|
||||
# This should not have a trailing slash, the endpoint above will be appended to the provided address.
|
||||
public_address: null
|
||||
# A random key used to sign the avatar URLs. The bridge will only accept requests with a valid signature.
|
||||
avatar_proxy_key: generate
|
||||
|
||||
portal_message_buffer: 128
|
||||
|
||||
# Number of private channel portals to create on bridge startup.
|
||||
|
|
4
go.mod
4
go.mod
|
@ -14,11 +14,11 @@ require (
|
|||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/yuin/goldmark v1.6.0
|
||||
go.mau.fi/util v0.2.1
|
||||
go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848
|
||||
golang.org/x/sync v0.5.0
|
||||
maunium.net/go/maulogger/v2 v2.4.1
|
||||
maunium.net/go/mautrix v0.16.3-0.20240218195727-4ceb1123b660
|
||||
maunium.net/go/mautrix v0.16.3-0.20240712164054-e6046fbf432c
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
8
go.sum
8
go.sum
|
@ -45,8 +45,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
|||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
|
||||
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw=
|
||||
go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c=
|
||||
go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb h1:Is+6vDKgINRy9KHodvi7NElxoDaWA8sc2S3cF3+QWjs=
|
||||
go.mau.fi/util v0.2.2-0.20231228160422-22fdd4bbddeb/go.mod h1:tiBX6nxVSOjU89jVQ7wBh3P8KjM26Lv1k7/I5QdSvBw=
|
||||
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
|
||||
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||
|
@ -72,5 +72,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
|||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
|
||||
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
|
||||
maunium.net/go/mautrix v0.16.3-0.20240218195727-4ceb1123b660 h1:ZPg1i0wsyEs5ee7z6Gn4mSRsq9BtDV//rfYTGs82l8c=
|
||||
maunium.net/go/mautrix v0.16.3-0.20240218195727-4ceb1123b660/go.mod h1:YL4l4rZB46/vj/ifRMEjcibbvHjgxHftOF1SgmruLu4=
|
||||
maunium.net/go/mautrix v0.16.3-0.20240712164054-e6046fbf432c h1:LHjqti3fFzrC8LXkkxxKYlLbuI/CJcwa2JN4Ppg2GK0=
|
||||
maunium.net/go/mautrix v0.16.3-0.20240712164054-e6046fbf432c/go.mod h1:gCgLw/4c1a8QsiOWTdUdXlt5cYdE0rJ9wLeZQKPD58Q=
|
||||
|
|
4
main.go
4
main.go
|
@ -18,6 +18,7 @@ package main
|
|||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"go.mau.fi/util/configupgrade"
|
||||
|
@ -105,6 +106,9 @@ func (br *DiscordBridge) Start() {
|
|||
if br.Config.Bridge.Provisioning.SharedSecret != "disable" {
|
||||
br.provisioning = newProvisioningAPI(br)
|
||||
}
|
||||
if br.Config.Bridge.PublicAddress != "" {
|
||||
br.AS.Router.HandleFunc("/mautrix-discord/avatar/{server}/{mediaID}/{checksum}", br.serveMediaProxy).Methods(http.MethodGet)
|
||||
}
|
||||
br.DMA = newDirectMediaAPI(br)
|
||||
br.WaitWebsocketConnected()
|
||||
go br.startUsers()
|
||||
|
|
70
portal.go
70
portal.go
|
@ -3,9 +3,13 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
@ -17,6 +21,7 @@ import (
|
|||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/util/variationselector"
|
||||
|
@ -1370,6 +1375,64 @@ func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part strin
|
|||
}
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) serveMediaProxy(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
mxc := id.ContentURI{
|
||||
Homeserver: vars["server"],
|
||||
FileID: vars["mediaID"],
|
||||
}
|
||||
checksum, err := base64.RawURLEncoding.DecodeString(vars["checksum"])
|
||||
if err != nil || len(checksum) != 32 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
_, expectedChecksum := br.hashMediaProxyURL(mxc)
|
||||
if !hmac.Equal(checksum, expectedChecksum) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
reader, err := br.Bot.Download(mxc)
|
||||
if err != nil {
|
||||
br.ZLog.Warn().Err(err).Msg("Failed to download media to proxy")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 32*1024)
|
||||
n, err := io.ReadFull(reader, buf)
|
||||
if err != nil && (!errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF)) {
|
||||
br.ZLog.Warn().Err(err).Msg("Failed to read first part of media to proxy")
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Add("Content-Type", http.DetectContentType(buf[:n]))
|
||||
if n < len(buf) {
|
||||
w.Header().Add("Content-Length", strconv.Itoa(n))
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = w.Write(buf[:n])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if n >= len(buf) {
|
||||
_, _ = io.CopyBuffer(w, reader, buf)
|
||||
}
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) hashMediaProxyURL(mxc id.ContentURI) (string, []byte) {
|
||||
path := fmt.Sprintf("/mautrix-discord/avatar/%s/%s/", mxc.Homeserver, mxc.FileID)
|
||||
checksum := hmac.New(sha256.New, []byte(br.Config.Bridge.AvatarProxyKey))
|
||||
checksum.Write([]byte(path))
|
||||
return path, checksum.Sum(nil)
|
||||
}
|
||||
|
||||
func (br *DiscordBridge) makeMediaProxyURL(mxc id.ContentURI) string {
|
||||
if br.Config.Bridge.PublicAddress == "" {
|
||||
return ""
|
||||
}
|
||||
path, checksum := br.hashMediaProxyURL(mxc)
|
||||
return br.Config.Bridge.PublicAddress + path + base64.RawURLEncoding.EncodeToString(checksum)
|
||||
}
|
||||
|
||||
func (portal *Portal) getRelayUserMeta(sender *User) (name, avatarURL string) {
|
||||
member := portal.bridge.StateStore.GetMember(portal.MXID, sender.MXID)
|
||||
name = member.Displayname
|
||||
|
@ -1377,11 +1440,8 @@ func (portal *Portal) getRelayUserMeta(sender *User) (name, avatarURL string) {
|
|||
name = sender.MXID.String()
|
||||
}
|
||||
mxc := member.AvatarURL.ParseOrIgnore()
|
||||
if !mxc.IsEmpty() {
|
||||
avatarURL = mautrix.BuildURL(
|
||||
portal.bridge.PublicHSAddress,
|
||||
"_matrix", "media", "v3", "download", mxc.Homeserver, mxc.FileID,
|
||||
).String()
|
||||
if !mxc.IsEmpty() && portal.bridge.Config.Bridge.PublicAddress != "" {
|
||||
avatarURL = portal.bridge.makeMediaProxyURL(mxc)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue