Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 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,15 @@ 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 != "" {
normalized, err := matrix.NormalizeServerName(config.MatrixServerName)
if err != nil {
return errors.Wrap(err, "invalid Matrix Server Name")
}
config.MatrixServerName = normalized
}

return nil
}

Expand All @@ -130,6 +140,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