From 1cf9643bd09313b687c749c87a2c36a56d8329a5 Mon Sep 17 00:00:00 2001 From: wiggin77 Date: Fri, 19 Dec 2025 22:24:06 -0500 Subject: [PATCH 1/5] Support Matrix server discovery (.well-known) --- plugin.json | 8 + server/bridge_utils.go | 17 +- server/bridge_utils_test.go | 12 +- server/command/command.go | 22 +- server/command/command_test.go | 4 + server/configuration.go | 18 ++ server/matrix/client.go | 51 +++-- server/matrix/client_ratelimit_test.go | 18 +- server/matrix/ratelimit_load_test.go | 8 +- server/matrix/server_discovery.go | 156 +++++++++++++ server/matrix/server_discovery_test.go | 293 +++++++++++++++++++++++++ server/matrix/test/client_test.go | 5 + server/plugin.go | 9 +- server/testhelpers_test.go | 2 +- testcontainers/matrix/container.go | 1 + 15 files changed, 562 insertions(+), 62 deletions(-) create mode 100644 server/matrix/server_discovery.go create mode 100644 server/matrix/server_discovery_test.go diff --git a/plugin.json b/plugin.json index 9e1dc0e..9adc558 100644 --- a/plugin.json +++ b/plugin.json @@ -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", diff --git a/server/bridge_utils.go b/server/bridge_utils.go index 24981a5..35edb39 100644 --- a/server/bridge_utils.go +++ b/server/bridge_utils.go @@ -451,16 +451,19 @@ 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, falling back to hostname extraction", "error", err) + // Fallback to simple hostname extraction + serverName, _ = matrix.ExtractServerDomain(serverURL) } // Reconstruct the full Matrix user ID - return "@" + matrixUsername + ":" + serverDomain + return "@" + matrixUsername + ":" + serverName } diff --git a/server/bridge_utils_test.go b/server/bridge_utils_test.go index 934a2ca..54e72d3 100644 --- a/server/bridge_utils_test.go +++ b/server/bridge_utils_test.go @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/server/command/command.go b/server/command/command.go index 81f72e4..67c4c22 100644 --- a/server/command/command.go +++ b/server/command/command.go @@ -3,7 +3,6 @@ package command import ( "fmt" - "net/url" "strings" "github.com/mattermost/mattermost-plugin-matrix-bridge/server/matrix" @@ -17,6 +16,7 @@ import ( // Configuration interface for accessing plugin configuration type Configuration interface { GetMatrixServerURL() string + GetMatrixServerName() string GetMatrixUsernamePrefixForServer(serverURL string) string } @@ -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 { diff --git a/server/command/command_test.go b/server/command/command_test.go index eaeafe3..091822b 100644 --- a/server/command/command_test.go +++ b/server/command/command_test.go @@ -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 } diff --git a/server/configuration.go b/server/configuration.go index 195cd45..ec27727 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -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"` @@ -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") + } + } + return nil } @@ -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 == "" { diff --git a/server/matrix/client.go b/server/matrix/client.go index 2757748..b4216e9 100644 --- a/server/matrix/client.go +++ b/server/matrix/client.go @@ -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 @@ -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 @@ -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 } @@ -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 } @@ -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 diff --git a/server/matrix/client_ratelimit_test.go b/server/matrix/client_ratelimit_test.go index bfc16d6..b5fd4e7 100644 --- a/server/matrix/client_ratelimit_test.go +++ b/server/matrix/client_ratelimit_test.go @@ -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", @@ -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() @@ -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", @@ -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", @@ -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", @@ -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", @@ -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() @@ -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") @@ -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", diff --git a/server/matrix/ratelimit_load_test.go b/server/matrix/ratelimit_load_test.go index 859e2cb..c14f833 100644 --- a/server/matrix/ratelimit_load_test.go +++ b/server/matrix/ratelimit_load_test.go @@ -91,7 +91,7 @@ func TestClient_MessageSpamLoad(t *testing.T) { config := LoadTestRateLimitConfig() 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: "!spam-test:example.invalid", @@ -184,7 +184,7 @@ func TestClient_RoomCreationSpamLoad(t *testing.T) { config := LoadTestRateLimitConfig() 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) const numCreators = 10 const roomsPerCreator = 5 @@ -266,7 +266,7 @@ func TestClient_MixedOperationLoad(t *testing.T) { config := LoadTestRateLimitConfig() 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) const numWorkers = 15 const operationsPerWorker = 10 @@ -451,7 +451,7 @@ func TestClient_RateLimitingEffectiveness_Integration(t *testing.T) { // Use standard test config which provides fast but consistent throttling validation config := TestRateLimitConfig() 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) // Calculate expected timing thresholds from actual config var expectedThrottleDelay time.Duration diff --git a/server/matrix/server_discovery.go b/server/matrix/server_discovery.go new file mode 100644 index 0000000..8bb4a74 --- /dev/null +++ b/server/matrix/server_discovery.go @@ -0,0 +1,156 @@ +package matrix + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/pkg/errors" +) + +// WellKnownResponse represents the response from /.well-known/matrix/server +type WellKnownResponse struct { + Server string `json:"m.server"` +} + +// ServerDiscovery handles Matrix server name discovery +type ServerDiscovery struct { + logger Logger + httpClient *http.Client +} + +// NewServerDiscovery creates a new ServerDiscovery instance +func NewServerDiscovery(logger Logger) *ServerDiscovery { + return &ServerDiscovery{ + logger: logger, + httpClient: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +// DiscoverServerName discovers the Matrix server name (domain for Matrix IDs) using the following chain: +// 1. Use configuredServerName if provided (manual configuration) +// 2. Try .well-known discovery on the serverURL hostname +// 3. Fall back to extracting hostname from serverURL +// +// Returns the server name to use in Matrix IDs (e.g., "example.com") +func (sd *ServerDiscovery) DiscoverServerName(serverURL, configuredServerName string) (string, error) { + // 1. If configured, use that + if configuredServerName != "" { + sd.logger.LogDebug("Using configured server name", "server_name", configuredServerName) + return configuredServerName, nil + } + + // 2. Parse the server URL to get hostname + parsedURL, err := url.Parse(serverURL) + if err != nil { + return "", errors.Wrap(err, "failed to parse server URL") + } + + hostname := parsedURL.Hostname() + if hostname == "" { + return "", errors.New("could not extract hostname from server URL") + } + + // 3. Try .well-known discovery + wellKnownServerName, err := sd.tryWellKnownDiscovery(hostname) + if err == nil && wellKnownServerName != "" { + sd.logger.LogDebug("Discovered server name via .well-known", "hostname", hostname, "server_name", wellKnownServerName) + return wellKnownServerName, nil + } + + // Log discovery failure but continue with fallback + if err != nil { + sd.logger.LogDebug("Failed to discover server name via .well-known, using hostname fallback", "hostname", hostname, "error", err.Error()) + } + + // 4. Fall back to using hostname as server name + sd.logger.LogDebug("Using hostname as server name (fallback)", "server_name", hostname) + return hostname, nil +} + +// tryWellKnownDiscovery attempts to discover the Matrix server name via .well-known +// Returns the server name if discovery succeeds, empty string and error otherwise +func (sd *ServerDiscovery) tryWellKnownDiscovery(hostname string) (string, error) { + // Construct .well-known URL + wellKnownURL := fmt.Sprintf("https://%s/.well-known/matrix/server", hostname) + + sd.logger.LogDebug("Attempting .well-known server discovery", "url", wellKnownURL) + + // Make HTTP request + resp, err := sd.httpClient.Get(wellKnownURL) + if err != nil { + return "", errors.Wrap(err, "failed to fetch .well-known") + } + defer resp.Body.Close() + + // Check status code + if resp.StatusCode != http.StatusOK { + return "", errors.Errorf(".well-known returned status %d", resp.StatusCode) + } + + // Read and limit response body + limitedBody := io.LimitReader(resp.Body, 10*1024) // 10KB limit + body, err := io.ReadAll(limitedBody) + if err != nil { + return "", errors.Wrap(err, "failed to read .well-known response") + } + + // Parse JSON response + var wellKnown WellKnownResponse + if err := json.Unmarshal(body, &wellKnown); err != nil { + return "", errors.Wrap(err, "failed to parse .well-known JSON") + } + + // Validate response + if wellKnown.Server == "" { + return "", errors.New(".well-known response missing m.server field") + } + + // The .well-known response contains the actual homeserver location + // But the server name for Matrix IDs is the hostname we queried + // Example: querying example.com/.well-known returns {"m.server": "matrix.example.com:443"} + // Server name for IDs is: example.com + // Actual homeserver API is at: matrix.example.com + return hostname, nil +} + +// ExtractServerDomain is a utility function that extracts the server domain from a server URL +// This is used as a fallback when no manual configuration or discovery is available +func ExtractServerDomain(serverURL string) (string, error) { + if serverURL == "" { + return "", errors.New("server URL not configured") + } + + parsedURL, err := url.Parse(serverURL) + if err != nil { + return "", errors.Wrap(err, "failed to parse server URL") + } + + hostname := parsedURL.Hostname() + if hostname == "" { + return "", errors.New("could not extract hostname from server URL") + } + + return hostname, nil +} + +// NormalizeServerName ensures the server name is in the correct format +// Removes any protocol prefixes and trailing slashes +func NormalizeServerName(serverName string) string { + serverName = strings.TrimPrefix(serverName, "https://") + serverName = strings.TrimPrefix(serverName, "http://") + serverName = strings.TrimSuffix(serverName, "/") + + // Remove port if present (Matrix IDs don't include ports) + if idx := strings.Index(serverName, ":"); idx != -1 { + serverName = serverName[:idx] + } + + return serverName +} diff --git a/server/matrix/server_discovery_test.go b/server/matrix/server_discovery_test.go new file mode 100644 index 0000000..181c65e --- /dev/null +++ b/server/matrix/server_discovery_test.go @@ -0,0 +1,293 @@ +package matrix + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNormalizeServerName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Clean domain", + input: "example.com", + expected: "example.com", + }, + { + name: "Domain with https prefix", + input: "https://example.com", + expected: "example.com", + }, + { + name: "Domain with http prefix", + input: "http://example.com", + expected: "example.com", + }, + { + name: "Domain with trailing slash", + input: "example.com/", + expected: "example.com", + }, + { + name: "Domain with port (should remove port)", + input: "example.com:8008", + expected: "example.com", + }, + { + name: "Domain with protocol and port", + input: "https://example.com:8008", + expected: "example.com", + }, + { + name: "Domain with protocol, port, and trailing slash", + input: "https://example.com:8008/", + expected: "example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NormalizeServerName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestExtractServerDomain(t *testing.T) { + tests := []struct { + name string + serverURL string + expected string + expectError bool + }{ + { + name: "Valid HTTPS URL", + serverURL: "https://matrix.example.com", + expected: "matrix.example.com", + expectError: false, + }, + { + name: "Valid HTTPS URL with port", + serverURL: "https://matrix.example.com:8008", + expected: "matrix.example.com", + expectError: false, + }, + { + name: "Valid HTTP URL", + serverURL: "http://localhost:8008", + expected: "localhost", + expectError: false, + }, + { + name: "Empty URL", + serverURL: "", + expected: "", + expectError: true, + }, + { + name: "Invalid URL", + serverURL: "://invalid", + expected: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ExtractServerDomain(tt.serverURL) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestServerDiscoveryWithConfiguredServerName(t *testing.T) { + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + + serverName, err := discovery.DiscoverServerName("https://matrix.example.com", "example.com") + + require.NoError(t, err) + assert.Equal(t, "example.com", serverName, "Should use configured server name") +} + +func TestServerDiscoveryWithWellKnown(t *testing.T) { + // Create a test HTTP server that serves .well-known + wellKnownHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/.well-known/matrix/server" { + w.Header().Set("Content-Type", "application/json") + response := WellKnownResponse{ + Server: "matrix.example.com:443", + } + json.NewEncoder(w).Encode(response) + return + } + http.NotFound(w, r) + }) + + server := httptest.NewTLSServer(wellKnownHandler) + defer server.Close() + + // Extract hostname from test server URL + // Note: In real usage, the URL would be like "https://example.com" + // and .well-known would be at "https://example.com/.well-known/matrix/server" + // For testing, we're using the test server directly + + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + // Use the test server's custom HTTP client + discovery.httpClient = server.Client() + + // We can't easily test the full .well-known discovery with httptest + // because it requires hostname resolution, so we'll test the tryWellKnownDiscovery directly + // This test mainly validates the JSON parsing logic + + // For now, test that manual config works + serverName, err := discovery.DiscoverServerName("https://matrix.example.com", "example.com") + require.NoError(t, err) + assert.Equal(t, "example.com", serverName) +} + +func TestServerDiscoveryFallbackToHostname(t *testing.T) { + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + + // No configured server name, .well-known will fail for invalid domain + // Should fall back to hostname extraction + serverName, err := discovery.DiscoverServerName("https://matrix.example.com:8008", "") + + require.NoError(t, err) + assert.Equal(t, "matrix.example.com", serverName, "Should fall back to hostname from URL") +} + +func TestServerDiscoveryInvalidURL(t *testing.T) { + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + + _, err := discovery.DiscoverServerName("://invalid-url", "") + + assert.Error(t, err, "Should return error for invalid URL") +} + +func TestTryWellKnownDiscovery(t *testing.T) { + t.Run("Successful discovery", func(t *testing.T) { + // Create a test server that returns valid .well-known response + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/.well-known/matrix/server" { + w.Header().Set("Content-Type", "application/json") + response := WellKnownResponse{ + Server: "matrix.example.com:8448", + } + json.NewEncoder(w).Encode(response) + return + } + http.NotFound(w, r) + })) + defer server.Close() + + // Extract hostname from test server + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + + // We can't test this directly without modifying the hostname + // but we can test that the HTTP response is parsed correctly + // by calling the server directly + resp, err := discovery.httpClient.Get(server.URL + "/.well-known/matrix/server") + require.NoError(t, err) + defer resp.Body.Close() + + var wellKnown WellKnownResponse + err = json.NewDecoder(resp.Body).Decode(&wellKnown) + require.NoError(t, err) + assert.Equal(t, "matrix.example.com:8448", wellKnown.Server) + }) + + t.Run("404 Not Found", func(t *testing.T) { + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + + // Try a domain that definitely doesn't have .well-known + serverName, err := discovery.tryWellKnownDiscovery("nonexistent-test-domain-12345.invalid") + + assert.Error(t, err) + assert.Empty(t, serverName) + }) + + t.Run("Invalid JSON response", func(t *testing.T) { + // Create a test server that returns invalid JSON + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("invalid json")) + })) + defer server.Close() + + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + + // Extract hostname from test server URL for testing + // This won't work in practice but tests the JSON parsing + resp, err := discovery.httpClient.Get(server.URL + "/.well-known/matrix/server") + require.NoError(t, err) + defer resp.Body.Close() + + var wellKnown WellKnownResponse + err = json.NewDecoder(resp.Body).Decode(&wellKnown) + assert.Error(t, err, "Should fail to decode invalid JSON") + }) +} + +func TestServerDiscoveryIntegration(t *testing.T) { + tests := []struct { + name string + serverURL string + configuredServerName string + expectedServerName string + shouldAttemptWellKnown bool + }{ + { + name: "Configured server name provided", + serverURL: "https://matrix.example.com:8008", + configuredServerName: "example.com", + expectedServerName: "example.com", + shouldAttemptWellKnown: false, + }, + { + name: "No configured name, fallback to hostname", + serverURL: "https://matrix.example.com:8008", + configuredServerName: "", + expectedServerName: "matrix.example.com", + shouldAttemptWellKnown: true, + }, + { + name: "Clean URL without port", + serverURL: "https://matrix.org", + configuredServerName: "", + expectedServerName: "matrix.org", + shouldAttemptWellKnown: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := NewTestLogger(t) + discovery := NewServerDiscovery(logger) + + serverName, err := discovery.DiscoverServerName(tt.serverURL, tt.configuredServerName) + + require.NoError(t, err) + assert.Equal(t, tt.expectedServerName, serverName) + }) + } +} diff --git a/server/matrix/test/client_test.go b/server/matrix/test/client_test.go index ff343e3..1c0ea77 100644 --- a/server/matrix/test/client_test.go +++ b/server/matrix/test/client_test.go @@ -447,6 +447,7 @@ func (suite *MatrixClientTestSuite) TestMatrixClientErrorHandling() { "https://fake-matrix-server.invalid", // Fake URL - we're only testing empty token validation "", // Empty token "test-remote-id", + "", // No configured server name matrix.NewTestLogger(suite.T()), matrix.TestRateLimitConfig(), ) @@ -463,6 +464,7 @@ func (suite *MatrixClientTestSuite) TestMatrixClientErrorHandling() { "http://nonexistent.invalid:1234", suite.matrixContainer.ASToken, "test-remote-id", + "", matrix.NewTestLogger(suite.T()), matrix.TestRateLimitConfig(), ) @@ -566,6 +568,7 @@ func (suite *MatrixClientTestSuite) TestMatrixClientErrorHandling() { "http://localhost:99999", // Port that should be unused suite.matrixContainer.ASToken, "test-remote-id", + "", matrix.NewTestLogger(suite.T()), matrix.TestRateLimitConfig(), ) @@ -1154,6 +1157,7 @@ func TestMXCURIValidationErrorReporting(t *testing.T) { "https://matrix.example.com", "test-token", "test-remote-id", + "", matrix.NewTestLogger(t), matrix.TestRateLimitConfig(), ) @@ -1199,6 +1203,7 @@ func TestMXCURIValidationErrorReportingWithBetterErrorMessages(t *testing.T) { "https://matrix.example.com", "test-token", "test-remote-id", + "", matrix.NewTestLogger(t), matrix.TestRateLimitConfig(), ) diff --git a/server/plugin.go b/server/plugin.go index b9a1a62..5d54027 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -151,7 +151,14 @@ func (p *Plugin) initMatrixClient() { config := p.getConfiguration() rateLimitMode := matrix.ParseRateLimitingMode(config.RateLimitingMode) rateLimitConfig := matrix.GetRateLimitConfigByMode(rateLimitMode) - p.matrixClient = matrix.NewClientWithRateLimit(config.MatrixServerURL, config.MatrixASToken, p.remoteID, p.API, rateLimitConfig) + p.matrixClient = matrix.NewClientWithRateLimit( + config.MatrixServerURL, + config.MatrixASToken, + p.remoteID, + config.MatrixServerName, + p.API, + rateLimitConfig, + ) } func (p *Plugin) initBridges() { diff --git a/server/testhelpers_test.go b/server/testhelpers_test.go index a949f0e..dec9cd5 100644 --- a/server/testhelpers_test.go +++ b/server/testhelpers_test.go @@ -84,7 +84,7 @@ func setupPluginForTestWithLogger(t *testing.T, api plugin.API) *Plugin { // createMatrixClientWithTestLogger creates a matrix client with test logger and rate limiting for testing func createMatrixClientWithTestLogger(t *testing.T, serverURL, asToken, remoteID string) *matrix.Client { testLogger := matrix.NewTestLogger(t) - return matrix.NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, testLogger, matrix.TestRateLimitConfig()) + return matrix.NewClientWithLoggerAndRateLimit(serverURL, asToken, remoteID, "", testLogger, matrix.TestRateLimitConfig()) } // TestMatrixClientTestLogger verifies that matrix client uses test logger correctly diff --git a/testcontainers/matrix/container.go b/testcontainers/matrix/container.go index 36432c2..bd870a3 100644 --- a/testcontainers/matrix/container.go +++ b/testcontainers/matrix/container.go @@ -108,6 +108,7 @@ func StartMatrixContainer(t *testing.T, config MatrixTestConfig) *Container { serverURL, config.ASToken, "test-remote-id", + "", // No configured server name - will be set via SetServerDomain for testing matrix.NewTestLogger(t), matrix.TestRateLimitConfig(), ) From 3aa84afbb7a042f28205a27761979c97eac6429a Mon Sep 17 00:00:00 2001 From: wiggin77 Date: Fri, 19 Dec 2025 23:28:13 -0500 Subject: [PATCH 2/5] fix linter errors --- server/matrix/server_discovery.go | 4 +++- server/matrix/server_discovery_test.go | 16 ++++++++++------ server/matrix/test/client_test.go | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/server/matrix/server_discovery.go b/server/matrix/server_discovery.go index 8bb4a74..13cc6e6 100644 --- a/server/matrix/server_discovery.go +++ b/server/matrix/server_discovery.go @@ -87,7 +87,9 @@ func (sd *ServerDiscovery) tryWellKnownDiscovery(hostname string) (string, error if err != nil { return "", errors.Wrap(err, "failed to fetch .well-known") } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() // Check status code if resp.StatusCode != http.StatusOK { diff --git a/server/matrix/server_discovery_test.go b/server/matrix/server_discovery_test.go index 181c65e..58d7cd6 100644 --- a/server/matrix/server_discovery_test.go +++ b/server/matrix/server_discovery_test.go @@ -131,7 +131,7 @@ func TestServerDiscoveryWithWellKnown(t *testing.T) { response := WellKnownResponse{ Server: "matrix.example.com:443", } - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) return } http.NotFound(w, r) @@ -190,7 +190,7 @@ func TestTryWellKnownDiscovery(t *testing.T) { response := WellKnownResponse{ Server: "matrix.example.com:8448", } - json.NewEncoder(w).Encode(response) + _ = json.NewEncoder(w).Encode(response) return } http.NotFound(w, r) @@ -206,7 +206,9 @@ func TestTryWellKnownDiscovery(t *testing.T) { // by calling the server directly resp, err := discovery.httpClient.Get(server.URL + "/.well-known/matrix/server") require.NoError(t, err) - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() var wellKnown WellKnownResponse err = json.NewDecoder(resp.Body).Decode(&wellKnown) @@ -227,9 +229,9 @@ func TestTryWellKnownDiscovery(t *testing.T) { t.Run("Invalid JSON response", func(t *testing.T) { // Create a test server that returns invalid JSON - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") - w.Write([]byte("invalid json")) + _, _ = w.Write([]byte("invalid json")) })) defer server.Close() @@ -240,7 +242,9 @@ func TestTryWellKnownDiscovery(t *testing.T) { // This won't work in practice but tests the JSON parsing resp, err := discovery.httpClient.Get(server.URL + "/.well-known/matrix/server") require.NoError(t, err) - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() var wellKnown WellKnownResponse err = json.NewDecoder(resp.Body).Decode(&wellKnown) diff --git a/server/matrix/test/client_test.go b/server/matrix/test/client_test.go index 1c0ea77..75f7854 100644 --- a/server/matrix/test/client_test.go +++ b/server/matrix/test/client_test.go @@ -447,7 +447,7 @@ func (suite *MatrixClientTestSuite) TestMatrixClientErrorHandling() { "https://fake-matrix-server.invalid", // Fake URL - we're only testing empty token validation "", // Empty token "test-remote-id", - "", // No configured server name + "", // No configured server name matrix.NewTestLogger(suite.T()), matrix.TestRateLimitConfig(), ) From c8db8dd6f84c994d1c51d41ca4bcf1ada1396e04 Mon Sep 17 00:00:00 2001 From: wiggin77 Date: Sat, 20 Dec 2025 00:16:36 -0500 Subject: [PATCH 3/5] address review comments --- server/bridge_utils.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/server/bridge_utils.go b/server/bridge_utils.go index 35edb39..a5e9c40 100644 --- a/server/bridge_utils.go +++ b/server/bridge_utils.go @@ -459,9 +459,18 @@ func (s *BridgeUtils) reconstructMatrixUserIDFromUsername(mattermostUsername str discovery := matrix.NewServerDiscovery(logger) serverName, err := discovery.DiscoverServerName(serverURL, configuredServerName) if err != nil { - s.logger.LogWarn("Failed to discover server name, falling back to hostname extraction", "error", err) - // Fallback to simple hostname extraction - serverName, _ = matrix.ExtractServerDomain(serverURL) + 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 From 13615820a932a28649d33c95fa34bc17dd559ca0 Mon Sep 17 00:00:00 2001 From: wiggin77 Date: Sat, 20 Dec 2025 00:20:50 -0500 Subject: [PATCH 4/5] address review comments --- server/matrix/server_discovery.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/matrix/server_discovery.go b/server/matrix/server_discovery.go index 13cc6e6..6551af4 100644 --- a/server/matrix/server_discovery.go +++ b/server/matrix/server_discovery.go @@ -12,6 +12,12 @@ import ( "github.com/pkg/errors" ) +const ( + // maxWellKnownResponseSize is the maximum size in bytes for .well-known/matrix/server responses + // This prevents memory exhaustion from excessively large responses + maxWellKnownResponseSize = 10 * 1024 // 10KB +) + // WellKnownResponse represents the response from /.well-known/matrix/server type WellKnownResponse struct { Server string `json:"m.server"` @@ -97,7 +103,7 @@ func (sd *ServerDiscovery) tryWellKnownDiscovery(hostname string) (string, error } // Read and limit response body - limitedBody := io.LimitReader(resp.Body, 10*1024) // 10KB limit + limitedBody := io.LimitReader(resp.Body, maxWellKnownResponseSize) body, err := io.ReadAll(limitedBody) if err != nil { return "", errors.Wrap(err, "failed to read .well-known response") From 40478660bca335b9ae4e62b6420582b411f7809b Mon Sep 17 00:00:00 2001 From: wiggin77 Date: Wed, 11 Feb 2026 17:17:01 -0500 Subject: [PATCH 5/5] Address review comments --- server/configuration.go | 10 +++----- server/matrix/server_discovery.go | 34 +++++++++++++++++--------- server/matrix/server_discovery_test.go | 26 ++++++++++++++++---- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/server/configuration.go b/server/configuration.go index ec27727..2bc7f7a 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -125,13 +125,11 @@ func (p *Plugin) validateConfiguration(config *configuration) error { // 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") + normalized, err := matrix.NormalizeServerName(config.MatrixServerName) + if err != nil { + return errors.Wrap(err, "invalid Matrix Server Name") } + config.MatrixServerName = normalized } return nil diff --git a/server/matrix/server_discovery.go b/server/matrix/server_discovery.go index 6551af4..7062ba4 100644 --- a/server/matrix/server_discovery.go +++ b/server/matrix/server_discovery.go @@ -2,8 +2,8 @@ package matrix import ( "encoding/json" - "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -72,7 +72,7 @@ func (sd *ServerDiscovery) DiscoverServerName(serverURL, configuredServerName st // Log discovery failure but continue with fallback if err != nil { - sd.logger.LogDebug("Failed to discover server name via .well-known, using hostname fallback", "hostname", hostname, "error", err.Error()) + sd.logger.LogWarn("Failed to discover server name via .well-known, using hostname fallback", "hostname", hostname, "error", err.Error()) } // 4. Fall back to using hostname as server name @@ -84,7 +84,11 @@ func (sd *ServerDiscovery) DiscoverServerName(serverURL, configuredServerName st // Returns the server name if discovery succeeds, empty string and error otherwise func (sd *ServerDiscovery) tryWellKnownDiscovery(hostname string) (string, error) { // Construct .well-known URL - wellKnownURL := fmt.Sprintf("https://%s/.well-known/matrix/server", hostname) + wellKnownURL := (&url.URL{ + Scheme: "https", + Host: hostname, + Path: "/.well-known/matrix/server", + }).String() sd.logger.LogDebug("Attempting .well-known server discovery", "url", wellKnownURL) @@ -128,8 +132,10 @@ func (sd *ServerDiscovery) tryWellKnownDiscovery(hostname string) (string, error return hostname, nil } -// ExtractServerDomain is a utility function that extracts the server domain from a server URL -// This is used as a fallback when no manual configuration or discovery is available +// ExtractServerDomain extracts the hostname from a fully-qualified server URL +// (e.g., "https://matrix.example.com:8008/path" -> "matrix.example.com"). +// This expects a proper URL with a scheme and is used as a fallback when no +// manual configuration or .well-known discovery is available. func ExtractServerDomain(serverURL string) (string, error) { if serverURL == "" { return "", errors.New("server URL not configured") @@ -148,17 +154,23 @@ func ExtractServerDomain(serverURL string) (string, error) { return hostname, nil } -// NormalizeServerName ensures the server name is in the correct format -// Removes any protocol prefixes and trailing slashes -func NormalizeServerName(serverName string) string { +// NormalizeServerName sanitizes a user-provided server name for use in Matrix IDs. +// Unlike ExtractServerDomain which expects a full URL with scheme, this handles +// bare server names that may have been entered with accidental protocol prefixes, +// trailing slashes, or port numbers (e.g., "https://example.com:8008/" -> "example.com"). +func NormalizeServerName(serverName string) (string, error) { serverName = strings.TrimPrefix(serverName, "https://") serverName = strings.TrimPrefix(serverName, "http://") serverName = strings.TrimSuffix(serverName, "/") // Remove port if present (Matrix IDs don't include ports) - if idx := strings.Index(serverName, ":"); idx != -1 { - serverName = serverName[:idx] + if host, _, err := net.SplitHostPort(serverName); err == nil { + serverName = host } - return serverName + if serverName == "" { + return "", errors.New("server name is empty after normalization") + } + + return serverName, nil } diff --git a/server/matrix/server_discovery_test.go b/server/matrix/server_discovery_test.go index 58d7cd6..4639239 100644 --- a/server/matrix/server_discovery_test.go +++ b/server/matrix/server_discovery_test.go @@ -12,9 +12,10 @@ import ( func TestNormalizeServerName(t *testing.T) { tests := []struct { - name string - input string - expected string + name string + input string + expected string + expectError bool }{ { name: "Clean domain", @@ -51,12 +52,27 @@ func TestNormalizeServerName(t *testing.T) { input: "https://example.com:8008/", expected: "example.com", }, + { + name: "Empty string returns error", + input: "", + expectError: true, + }, + { + name: "Only protocol returns error", + input: "https://", + expectError: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := NormalizeServerName(tt.input) - assert.Equal(t, tt.expected, result) + result, err := NormalizeServerName(tt.input) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } }) } }