Skip to content

Commit ee791bf

Browse files
authored
Support Matrix server discovery (.well-known) (#27)
* Support Matrix server discovery (.well-known)
1 parent ba1ee60 commit ee791bf

15 files changed

Lines changed: 609 additions & 62 deletions

plugin.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@
3030
"placeholder": "https://matrix.example.com",
3131
"default": ""
3232
},
33+
{
34+
"key": "matrix_server_name",
35+
"display_name": "Matrix Server Name (Optional)",
36+
"type": "text",
37+
"help_text": "The domain used in Matrix IDs (e.g., example.com). Leave empty to automatically detect via .well-known/matrix/server or derive from the server URL. Only set this if your Matrix server uses a different domain for Matrix IDs than the homeserver URL hostname.",
38+
"placeholder": "example.com",
39+
"default": ""
40+
},
3341
{
3442
"key": "matrix_as_token",
3543
"display_name": "Matrix Application Service Token",

server/bridge_utils.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -451,16 +451,28 @@ func (s *BridgeUtils) reconstructMatrixUserIDFromUsername(mattermostUsername str
451451
return "" // Empty username
452452
}
453453

454-
// Extract server domain from Matrix server URL
454+
// Extract server domain using ServerDiscovery
455455
serverURL := config.GetMatrixServerURL()
456-
serverDomain := strings.TrimPrefix(serverURL, "https://")
457-
serverDomain = strings.TrimPrefix(serverDomain, "http://")
456+
configuredServerName := config.GetMatrixServerName()
458457

459-
// Remove any path components (e.g., "server.com:8008/_matrix" -> "server.com:8008")
460-
if idx := strings.Index(serverDomain, "/"); idx != -1 {
461-
serverDomain = serverDomain[:idx]
458+
logger := matrix.NewAPILogger(s.API)
459+
discovery := matrix.NewServerDiscovery(logger)
460+
serverName, err := discovery.DiscoverServerName(serverURL, configuredServerName)
461+
if err != nil {
462+
s.logger.LogWarn("Failed to discover server name; cannot reconstruct Matrix user ID",
463+
"error", err,
464+
"server_url", serverURL,
465+
"mattermost_username", mattermostUsername)
466+
return ""
467+
}
468+
469+
if serverName == "" {
470+
s.logger.LogWarn("Empty server name after discovery; cannot reconstruct Matrix user ID",
471+
"server_url", serverURL,
472+
"mattermost_username", mattermostUsername)
473+
return ""
462474
}
463475

464476
// Reconstruct the full Matrix user ID
465-
return "@" + matrixUsername + ":" + serverDomain
477+
return "@" + matrixUsername + ":" + serverName
466478
}

server/bridge_utils_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestExtractMatrixMessageContent(t *testing.T) {
1919

2020
logger := &testLogger{t: t}
2121
kvstore := NewMemoryKVStore()
22-
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
22+
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", "", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
2323

2424
config := BridgeUtilsConfig{
2525
Logger: logger,
@@ -234,7 +234,7 @@ func TestIsHTMLContent(t *testing.T) {
234234

235235
logger := &testLogger{t: t}
236236
kvstore := NewMemoryKVStore()
237-
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
237+
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", "", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
238238

239239
config := BridgeUtilsConfig{
240240
Logger: logger,
@@ -379,7 +379,7 @@ func TestExtractMattermostMetadata(t *testing.T) {
379379
api := &plugintest.API{}
380380
logger := &testLogger{t: t}
381381
kvstore := NewMemoryKVStore()
382-
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
382+
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", "", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
383383

384384
config := BridgeUtilsConfig{
385385
Logger: logger,
@@ -473,7 +473,7 @@ func TestIsGhostUser(t *testing.T) {
473473
api := &plugintest.API{}
474474
logger := &testLogger{t: t}
475475
kvstore := NewMemoryKVStore()
476-
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
476+
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", "", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
477477

478478
config := BridgeUtilsConfig{
479479
Logger: logger,
@@ -530,7 +530,7 @@ func TestExtractMentionedUsers(t *testing.T) {
530530
api := &plugintest.API{}
531531
logger := &testLogger{t: t}
532532
kvstore := NewMemoryKVStore()
533-
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
533+
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", "", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
534534

535535
config := BridgeUtilsConfig{
536536
Logger: logger,
@@ -624,7 +624,7 @@ func TestReplaceMatrixMentionHTML(t *testing.T) {
624624
api := &plugintest.API{}
625625
logger := &testLogger{t: t}
626626
kvstore := NewMemoryKVStore()
627-
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
627+
matrixClient := matrix.NewClientWithLoggerAndRateLimit("https://test.example.com", "test_token", "test_remote", "", matrix.NewTestLogger(t), matrix.UnitTestRateLimitConfig())
628628

629629
config := BridgeUtilsConfig{
630630
Logger: logger,

server/command/command.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package command
33

44
import (
55
"fmt"
6-
"net/url"
76
"strings"
87

98
"github.com/mattermost/mattermost-plugin-matrix-bridge/server/matrix"
@@ -17,6 +16,7 @@ import (
1716
// Configuration interface for accessing plugin configuration
1817
type Configuration interface {
1918
GetMatrixServerURL() string
19+
GetMatrixServerName() string
2020
GetMatrixUsernamePrefixForServer(serverURL string) string
2121
}
2222

@@ -900,20 +900,20 @@ func (c *Handler) extractServerDomain() string {
900900
return "matrix.org"
901901
}
902902

903-
// Parse the URL to extract the hostname
904-
parsedURL, err := url.Parse(serverURL)
905-
if err != nil {
906-
c.client.Log.Warn("Failed to parse Matrix server URL", "url", serverURL, "error", err)
907-
return "matrix.org"
908-
}
903+
// Get the configured server name (if set)
904+
configuredServerName := config.GetMatrixServerName()
909905

910-
hostname := parsedURL.Hostname()
911-
if hostname == "" {
912-
c.client.Log.Warn("Could not extract hostname from Matrix server URL", "url", serverURL)
906+
// Use ServerDiscovery to determine the server name
907+
// This will try: configured name -> .well-known discovery -> hostname fallback
908+
logger := matrix.NewAPILogger(c.pluginAPI)
909+
discovery := matrix.NewServerDiscovery(logger)
910+
serverName, err := discovery.DiscoverServerName(serverURL, configuredServerName)
911+
if err != nil {
912+
c.client.Log.Warn("Failed to discover Matrix server name", "url", serverURL, "error", err)
913913
return "matrix.org"
914914
}
915915

916-
return hostname
916+
return serverName
917917
}
918918

919919
func (c *Handler) executeTestCommand(_ *model.CommandArgs) *model.CommandResponse {

server/command/command_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ func (m *mockConfiguration) GetMatrixServerURL() string {
2626
return m.serverURL
2727
}
2828

29+
func (m *mockConfiguration) GetMatrixServerName() string {
30+
return "" // No configured server name in tests
31+
}
32+
2933
func (m *mockConfiguration) GetMatrixUsernamePrefixForServer(_ string) string {
3034
return "matrix" // Use default prefix for tests
3135
}

server/configuration.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const DefaultMatrixUsernamePrefix = "matrix"
2424
// copy appropriate for your types.
2525
type configuration struct {
2626
MatrixServerURL string `json:"matrix_server_url"`
27+
MatrixServerName string `json:"matrix_server_name"`
2728
MatrixASToken string `json:"matrix_as_token"`
2829
MatrixHSToken string `json:"matrix_hs_token"`
2930
EnableSync bool `json:"enable_sync"`
@@ -122,6 +123,15 @@ func (p *Plugin) validateConfiguration(config *configuration) error {
122123
parsedMode := matrix.ParseRateLimitingMode(config.RateLimitingMode)
123124
config.RateLimitingMode = string(parsedMode)
124125

126+
// Validate and normalize MatrixServerName if provided
127+
if config.MatrixServerName != "" {
128+
normalized, err := matrix.NormalizeServerName(config.MatrixServerName)
129+
if err != nil {
130+
return errors.Wrap(err, "invalid Matrix Server Name")
131+
}
132+
config.MatrixServerName = normalized
133+
}
134+
125135
return nil
126136
}
127137

@@ -130,6 +140,12 @@ func (c *configuration) GetMatrixServerURL() string {
130140
return c.MatrixServerURL
131141
}
132142

143+
// GetMatrixServerName returns the configured Matrix server name (domain for Matrix IDs)
144+
// If not set, this should be derived via server discovery
145+
func (c *configuration) GetMatrixServerName() string {
146+
return c.MatrixServerName
147+
}
148+
133149
// GetMatrixUsernamePrefix returns the username prefix to use for Matrix-originated users
134150
func (c *configuration) GetMatrixUsernamePrefix() string {
135151
if c.MatrixUsernamePrefix == "" {

server/matrix/client.go

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,14 @@ func (l *testLogger) LogError(message string, keyValuePairs ...any) {
180180

181181
// Client represents a Matrix HTTP client for communicating with Matrix servers.
182182
type Client struct {
183-
serverURL string
184-
asToken string // Application Service token for all operations
185-
remoteID string // Plugin remote ID for metadata
186-
httpClient *http.Client
187-
logger Logger
188-
serverDomain string // explicit server domain for testing
183+
serverURL string
184+
asToken string // Application Service token for all operations
185+
remoteID string // Plugin remote ID for metadata
186+
httpClient *http.Client
187+
logger Logger
188+
serverDomain string // override server domain for testing
189+
configuredServerName string // configured Matrix server name from config
190+
serverDiscovery *ServerDiscovery // utility for server name discovery
189191

190192
// Rate limiting
191193
rateLimitConfig RateLimitConfig
@@ -264,21 +266,23 @@ type SendEventResponse struct {
264266
}
265267

266268
// NewClientWithRateLimit creates a new Matrix client with custom rate limiting.
267-
func NewClientWithRateLimit(serverURL, asToken, remoteID string, api plugin.API, rateLimitConfig RateLimitConfig) *Client {
268-
return NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, NewAPILogger(api), rateLimitConfig)
269+
func NewClientWithRateLimit(serverURL, asToken, remoteID, configuredServerName string, api plugin.API, rateLimitConfig RateLimitConfig) *Client {
270+
return NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, configuredServerName, NewAPILogger(api), rateLimitConfig)
269271
}
270272

271273
// NewClientWithLoggerAndRateLimit creates a new Matrix client with custom logger and rate limiting.
272-
func NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID string, logger Logger, rateLimitConfig RateLimitConfig) *Client {
274+
func NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, configuredServerName string, logger Logger, rateLimitConfig RateLimitConfig) *Client {
273275
client := &Client{
274-
serverURL: serverURL,
275-
asToken: asToken,
276-
remoteID: remoteID,
276+
serverURL: serverURL,
277+
asToken: asToken,
278+
remoteID: remoteID,
279+
configuredServerName: configuredServerName,
277280
httpClient: &http.Client{
278281
Timeout: 30 * time.Second,
279282
},
280283
logger: logger,
281284
rateLimitConfig: rateLimitConfig,
285+
serverDiscovery: NewServerDiscovery(logger),
282286
}
283287

284288
// Initialize rate limiters if enabled
@@ -301,7 +305,7 @@ func NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID string, logger
301305
return client
302306
}
303307

304-
// SetServerDomain sets an explicit server domain (used for testing)
308+
// SetServerDomain sets an override server domain (used for testing)
305309
func (c *Client) SetServerDomain(domain string) {
306310
c.serverDomain = domain
307311
}
@@ -1088,9 +1092,14 @@ func (c *Client) CreateDirectRoom(ghostUserIDs []string, roomName string) (strin
10881092
return response.RoomID, nil
10891093
}
10901094

1091-
// extractServerDomain extracts the hostname from the Matrix server URL
1095+
// extractServerDomain extracts the Matrix server name (domain for Matrix IDs)
1096+
// It uses the following chain:
1097+
// 1. Override server domain (for testing)
1098+
// 2. Configured server name from plugin config
1099+
// 3. .well-known discovery
1100+
// 4. Fallback to hostname extraction from server URL
10921101
func (c *Client) extractServerDomain() (string, error) {
1093-
// Use explicit server domain if set (for testing)
1102+
// Use override server domain if set (for testing)
10941103
if c.serverDomain != "" {
10951104
return c.serverDomain, nil
10961105
}
@@ -1099,17 +1108,13 @@ func (c *Client) extractServerDomain() (string, error) {
10991108
return "", errors.New("server URL not configured")
11001109
}
11011110

1102-
parsedURL, err := url.Parse(c.serverURL)
1111+
// Use ServerDiscovery to determine the server name
1112+
serverName, err := c.serverDiscovery.DiscoverServerName(c.serverURL, c.configuredServerName)
11031113
if err != nil {
1104-
return "", errors.Wrap(err, "failed to parse server URL")
1114+
return "", errors.Wrap(err, "failed to discover server name")
11051115
}
11061116

1107-
hostname := parsedURL.Hostname()
1108-
if hostname == "" {
1109-
return "", errors.New("could not extract hostname from server URL")
1110-
}
1111-
1112-
return hostname, nil
1117+
return serverName, nil
11131118
}
11141119

11151120
// AddRoomAlias adds an additional alias to an existing Matrix room

server/matrix/client_ratelimit_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestClient_SendMessage_RateLimiting(t *testing.T) {
2424
}
2525

2626
logger := NewTestLogger(t)
27-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
27+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
2828

2929
req := MessageRequest{
3030
RoomID: "!test:example.invalid",
@@ -66,7 +66,7 @@ func TestClient_CreateRoom_RateLimiting(t *testing.T) {
6666
}
6767

6868
logger := NewTestLogger(t)
69-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
69+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
7070

7171
// First room creation should succeed quickly
7272
start := time.Now()
@@ -101,7 +101,7 @@ func TestClient_ConcurrentMessageSending_RateLimiting(t *testing.T) {
101101
}
102102

103103
logger := NewTestLogger(t)
104-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
104+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
105105

106106
req := MessageRequest{
107107
RoomID: "!test:example.invalid",
@@ -155,7 +155,7 @@ func TestClient_RateLimiting_Disabled(t *testing.T) {
155155
config := GetRateLimitConfigByMode(RateLimitDisabled)
156156

157157
logger := NewTestLogger(t)
158-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
158+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
159159

160160
req := MessageRequest{
161161
RoomID: "!test:example.invalid",
@@ -188,7 +188,7 @@ func TestClient_RateLimiting_ContextTimeout(t *testing.T) {
188188
}
189189

190190
logger := NewTestLogger(t)
191-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
191+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
192192

193193
req := MessageRequest{
194194
RoomID: "!test:example.invalid",
@@ -233,7 +233,7 @@ func TestClient_TokenBucketBurstBehavior(t *testing.T) {
233233
}
234234

235235
logger := NewTestLogger(t)
236-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
236+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
237237

238238
req := MessageRequest{
239239
RoomID: "!test:example.invalid",
@@ -279,7 +279,7 @@ func TestClient_MixedOperations_IndependentRateLimiting(t *testing.T) {
279279
}
280280

281281
logger := NewTestLogger(t)
282-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
282+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
283283

284284
// Send message (consumes message rate limit)
285285
start := time.Now()
@@ -324,7 +324,7 @@ func TestClient_RateLimitError_Detection(t *testing.T) {
324324

325325
config := UnitTestRateLimitConfig() // Use unit test config with predictable values
326326
logger := NewTestLogger(t)
327-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
327+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
328328

329329
// Verify that rate limiters are properly initialized
330330
assert.NotNil(t, client.messageLimiter, "Message limiter should be initialized")
@@ -422,7 +422,7 @@ func BenchmarkClient_SendMessage_WithRateLimit(b *testing.B) {
422422
config := GetRateLimitConfigByMode(RateLimitDisabled)
423423

424424
logger := NewTestLogger(b)
425-
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
425+
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)
426426

427427
req := MessageRequest{
428428
RoomID: "!test:example.invalid",

0 commit comments

Comments
 (0)