Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@
"placeholder": "https://matrix.example.com",
"default": ""
},
{
"key": "matrix_server_name",
"display_name": "Matrix Server Name (Optional)",
"type": "text",
"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.",
"placeholder": "example.com",
"default": ""
},
{
"key": "matrix_as_token",
"display_name": "Matrix Application Service Token",
Expand Down
26 changes: 19 additions & 7 deletions server/bridge_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,16 +451,28 @@ func (s *BridgeUtils) reconstructMatrixUserIDFromUsername(mattermostUsername str
return "" // Empty username
}

// Extract server domain from Matrix server URL
// Extract server domain using ServerDiscovery
serverURL := config.GetMatrixServerURL()
serverDomain := strings.TrimPrefix(serverURL, "https://")
serverDomain = strings.TrimPrefix(serverDomain, "http://")
configuredServerName := config.GetMatrixServerName()

// Remove any path components (e.g., "server.com:8008/_matrix" -> "server.com:8008")
if idx := strings.Index(serverDomain, "/"); idx != -1 {
serverDomain = serverDomain[:idx]
logger := matrix.NewAPILogger(s.API)
discovery := matrix.NewServerDiscovery(logger)
serverName, err := discovery.DiscoverServerName(serverURL, configuredServerName)
if err != nil {
s.logger.LogWarn("Failed to discover server name; cannot reconstruct Matrix user ID",
"error", err,
"server_url", serverURL,
"mattermost_username", mattermostUsername)
return ""
}

if serverName == "" {
s.logger.LogWarn("Empty server name after discovery; cannot reconstruct Matrix user ID",
"server_url", serverURL,
"mattermost_username", mattermostUsername)
return ""
}

// Reconstruct the full Matrix user ID
return "@" + matrixUsername + ":" + serverDomain
return "@" + matrixUsername + ":" + serverName
}
12 changes: 6 additions & 6 deletions server/bridge_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestExtractMatrixMessageContent(t *testing.T) {

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

config := BridgeUtilsConfig{
Logger: logger,
Expand Down Expand Up @@ -234,7 +234,7 @@ func TestIsHTMLContent(t *testing.T) {

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

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

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

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

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

config := BridgeUtilsConfig{
Logger: logger,
Expand Down
22 changes: 11 additions & 11 deletions server/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package command

import (
"fmt"
"net/url"
"strings"
Comment thread
wiggin77 marked this conversation as resolved.

"github.com/mattermost/mattermost-plugin-matrix-bridge/server/matrix"
Expand All @@ -17,6 +16,7 @@ import (
// Configuration interface for accessing plugin configuration
type Configuration interface {
GetMatrixServerURL() string
GetMatrixServerName() string
GetMatrixUsernamePrefixForServer(serverURL string) string
}

Expand Down Expand Up @@ -900,20 +900,20 @@ func (c *Handler) extractServerDomain() string {
return "matrix.org"
}

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

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

return hostname
return serverName
}

func (c *Handler) executeTestCommand(_ *model.CommandArgs) *model.CommandResponse {
Expand Down
4 changes: 4 additions & 0 deletions server/command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func (m *mockConfiguration) GetMatrixServerURL() string {
return m.serverURL
}

func (m *mockConfiguration) GetMatrixServerName() string {
return "" // No configured server name in tests
}

func (m *mockConfiguration) GetMatrixUsernamePrefixForServer(_ string) string {
return "matrix" // Use default prefix for tests
}
Expand Down
18 changes: 18 additions & 0 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const DefaultMatrixUsernamePrefix = "matrix"
// copy appropriate for your types.
type configuration struct {
MatrixServerURL string `json:"matrix_server_url"`
MatrixServerName string `json:"matrix_server_name"`
MatrixASToken string `json:"matrix_as_token"`
MatrixHSToken string `json:"matrix_hs_token"`
EnableSync bool `json:"enable_sync"`
Expand Down Expand Up @@ -122,6 +123,17 @@ func (p *Plugin) validateConfiguration(config *configuration) error {
parsedMode := matrix.ParseRateLimitingMode(config.RateLimitingMode)
config.RateLimitingMode = string(parsedMode)

// Validate and normalize MatrixServerName if provided
if config.MatrixServerName != "" {
// Normalize the server name (remove protocol, trailing slashes, etc.)
config.MatrixServerName = matrix.NormalizeServerName(config.MatrixServerName)

// Basic validation: should not be empty after normalization
if config.MatrixServerName == "" {
return errors.New("Matrix Server Name is invalid after normalization")
Comment thread
wiggin77 marked this conversation as resolved.
Outdated
}
}

return nil
}

Expand All @@ -130,6 +142,12 @@ func (c *configuration) GetMatrixServerURL() string {
return c.MatrixServerURL
}

// GetMatrixServerName returns the configured Matrix server name (domain for Matrix IDs)
// If not set, this should be derived via server discovery
func (c *configuration) GetMatrixServerName() string {
return c.MatrixServerName
}

// GetMatrixUsernamePrefix returns the username prefix to use for Matrix-originated users
func (c *configuration) GetMatrixUsernamePrefix() string {
if c.MatrixUsernamePrefix == "" {
Expand Down
51 changes: 28 additions & 23 deletions server/matrix/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,14 @@ func (l *testLogger) LogError(message string, keyValuePairs ...any) {

// Client represents a Matrix HTTP client for communicating with Matrix servers.
type Client struct {
serverURL string
asToken string // Application Service token for all operations
remoteID string // Plugin remote ID for metadata
httpClient *http.Client
logger Logger
serverDomain string // explicit server domain for testing
serverURL string
asToken string // Application Service token for all operations
remoteID string // Plugin remote ID for metadata
httpClient *http.Client
logger Logger
serverDomain string // override server domain for testing
configuredServerName string // configured Matrix server name from config
serverDiscovery *ServerDiscovery // utility for server name discovery

// Rate limiting
rateLimitConfig RateLimitConfig
Expand Down Expand Up @@ -264,21 +266,23 @@ type SendEventResponse struct {
}

// NewClientWithRateLimit creates a new Matrix client with custom rate limiting.
func NewClientWithRateLimit(serverURL, asToken, remoteID string, api plugin.API, rateLimitConfig RateLimitConfig) *Client {
return NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, NewAPILogger(api), rateLimitConfig)
func NewClientWithRateLimit(serverURL, asToken, remoteID, configuredServerName string, api plugin.API, rateLimitConfig RateLimitConfig) *Client {
return NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, configuredServerName, NewAPILogger(api), rateLimitConfig)
}

// NewClientWithLoggerAndRateLimit creates a new Matrix client with custom logger and rate limiting.
func NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID string, logger Logger, rateLimitConfig RateLimitConfig) *Client {
func NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, configuredServerName string, logger Logger, rateLimitConfig RateLimitConfig) *Client {
client := &Client{
serverURL: serverURL,
asToken: asToken,
remoteID: remoteID,
serverURL: serverURL,
asToken: asToken,
remoteID: remoteID,
configuredServerName: configuredServerName,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
logger: logger,
rateLimitConfig: rateLimitConfig,
serverDiscovery: NewServerDiscovery(logger),
}

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

// SetServerDomain sets an explicit server domain (used for testing)
// SetServerDomain sets an override server domain (used for testing)
func (c *Client) SetServerDomain(domain string) {
c.serverDomain = domain
}
Expand Down Expand Up @@ -1088,9 +1092,14 @@ func (c *Client) CreateDirectRoom(ghostUserIDs []string, roomName string) (strin
return response.RoomID, nil
}

// extractServerDomain extracts the hostname from the Matrix server URL
// extractServerDomain extracts the Matrix server name (domain for Matrix IDs)
// It uses the following chain:
// 1. Override server domain (for testing)
// 2. Configured server name from plugin config
// 3. .well-known discovery
// 4. Fallback to hostname extraction from server URL
func (c *Client) extractServerDomain() (string, error) {
// Use explicit server domain if set (for testing)
// Use override server domain if set (for testing)
if c.serverDomain != "" {
return c.serverDomain, nil
}
Expand All @@ -1099,17 +1108,13 @@ func (c *Client) extractServerDomain() (string, error) {
return "", errors.New("server URL not configured")
}

parsedURL, err := url.Parse(c.serverURL)
// Use ServerDiscovery to determine the server name
serverName, err := c.serverDiscovery.DiscoverServerName(c.serverURL, c.configuredServerName)
if err != nil {
return "", errors.Wrap(err, "failed to parse server URL")
return "", errors.Wrap(err, "failed to discover server name")
}

hostname := parsedURL.Hostname()
if hostname == "" {
return "", errors.New("could not extract hostname from server URL")
}

return hostname, nil
return serverName, nil
}

// AddRoomAlias adds an additional alias to an existing Matrix room
Expand Down
18 changes: 9 additions & 9 deletions server/matrix/client_ratelimit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestClient_SendMessage_RateLimiting(t *testing.T) {
}

logger := NewTestLogger(t)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

req := MessageRequest{
RoomID: "!test:example.invalid",
Expand Down Expand Up @@ -66,7 +66,7 @@ func TestClient_CreateRoom_RateLimiting(t *testing.T) {
}

logger := NewTestLogger(t)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

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

logger := NewTestLogger(t)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

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

logger := NewTestLogger(t)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

req := MessageRequest{
RoomID: "!test:example.invalid",
Expand Down Expand Up @@ -188,7 +188,7 @@ func TestClient_RateLimiting_ContextTimeout(t *testing.T) {
}

logger := NewTestLogger(t)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

req := MessageRequest{
RoomID: "!test:example.invalid",
Expand Down Expand Up @@ -233,7 +233,7 @@ func TestClient_TokenBucketBurstBehavior(t *testing.T) {
}

logger := NewTestLogger(t)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

req := MessageRequest{
RoomID: "!test:example.invalid",
Expand Down Expand Up @@ -279,7 +279,7 @@ func TestClient_MixedOperations_IndependentRateLimiting(t *testing.T) {
}

logger := NewTestLogger(t)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

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

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

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

logger := NewTestLogger(b)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", logger, config)
client := NewClientWithLoggerAndRateLimit("http://localhost:1", "test_token", "test_remote", "", logger, config)

req := MessageRequest{
RoomID: "!test:example.invalid",
Expand Down
Loading
Loading