- Add XEPFeatures framework for managing XMPP extension protocols - Implement complete XEP-0077 In-Band Registration functionality - Add server capability detection using disco#info queries - Only initialize XEP features when server supports them - Add comprehensive XEP-0077 testing to doctor command - Doctor tests create and delete test users to validate functionality - Add struct-based XEP management instead of dynamic maps 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
592 lines
18 KiB
Go
592 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/xmpp"
|
|
)
|
|
|
|
const (
|
|
// Default values for development server (sidecar)
|
|
defaultServer = "localhost:5222"
|
|
defaultUsername = "testuser@localhost"
|
|
defaultPassword = "testpass"
|
|
defaultResource = "doctor"
|
|
defaultTestRoom = "test1@conference.localhost"
|
|
)
|
|
|
|
type Config struct {
|
|
Server string
|
|
Username string
|
|
Password string
|
|
Resource string
|
|
TestRoom string
|
|
TestMUC bool
|
|
TestDirectMessage bool
|
|
TestRoomExists bool
|
|
TestXEP0077 bool
|
|
Verbose bool
|
|
InsecureSkipVerify bool
|
|
}
|
|
|
|
func main() {
|
|
config := &Config{}
|
|
|
|
// Define command line flags
|
|
flag.StringVar(&config.Server, "server", defaultServer, "XMPP server address (host:port)")
|
|
flag.StringVar(&config.Username, "username", defaultUsername, "XMPP username/JID")
|
|
flag.StringVar(&config.Password, "password", defaultPassword, "XMPP password")
|
|
flag.StringVar(&config.Resource, "resource", defaultResource, "XMPP resource")
|
|
flag.StringVar(&config.TestRoom, "test-room", defaultTestRoom, "MUC room JID for testing")
|
|
flag.BoolVar(&config.TestMUC, "test-muc", true, "Enable MUC room testing (join/wait/leave)")
|
|
flag.BoolVar(&config.TestDirectMessage, "test-dm", true, "Enable direct message testing (send message to admin user)")
|
|
flag.BoolVar(&config.TestRoomExists, "test-room-exists", true, "Enable room existence testing using disco#info")
|
|
flag.BoolVar(&config.TestXEP0077, "test-xep0077", true, "Enable XEP-0077 In-Band Registration testing (required if enabled)")
|
|
flag.BoolVar(&config.Verbose, "verbose", true, "Enable verbose logging")
|
|
flag.BoolVar(&config.InsecureSkipVerify, "insecure-skip-verify", true, "Skip TLS certificate verification (for development)")
|
|
|
|
flag.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "xmpp-client-doctor - Test XMPP client connectivity and MUC operations\n\n")
|
|
fmt.Fprintf(os.Stderr, "This tool tests the XMPP client implementation by connecting to an XMPP server,\n")
|
|
fmt.Fprintf(os.Stderr, "performing connection tests, room existence checks, optionally testing MUC room operations\n")
|
|
fmt.Fprintf(os.Stderr, "and direct messages, and then disconnecting gracefully.\n\n")
|
|
fmt.Fprintf(os.Stderr, "Usage:\n")
|
|
fmt.Fprintf(os.Stderr, " %s [flags]\n\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, "Examples:\n")
|
|
fmt.Fprintf(os.Stderr, " %s # Test basic connectivity\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, " %s --test-muc # Test connectivity and MUC operations\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, " %s --test-dm # Test connectivity and direct messages\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, " %s --test-muc=false --test-dm=false # Test connectivity only\n\n", os.Args[0])
|
|
fmt.Fprintf(os.Stderr, "Flags:\n")
|
|
flag.PrintDefaults()
|
|
fmt.Fprintf(os.Stderr, "\nDefault values are configured for the development server in ./sidecar/\n")
|
|
fmt.Fprintf(os.Stderr, "Make sure to start the development server with: cd sidecar && docker-compose up -d\n")
|
|
fmt.Fprintf(os.Stderr, "For MUC testing, create the test room 'test1' via the admin console at http://localhost:9090\n")
|
|
}
|
|
|
|
flag.Parse()
|
|
|
|
if config.Verbose {
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
|
log.Printf("Starting XMPP client doctor...")
|
|
log.Printf("Configuration:")
|
|
log.Printf(" Server: %s", config.Server)
|
|
log.Printf(" Username: %s", config.Username)
|
|
log.Printf(" Resource: %s", config.Resource)
|
|
log.Printf(" Password: %s", maskPassword(config.Password))
|
|
if config.TestMUC {
|
|
log.Printf(" Test Room: %s", config.TestRoom)
|
|
}
|
|
if config.TestDirectMessage {
|
|
log.Printf(" Test Direct Messages: enabled")
|
|
}
|
|
if config.TestRoomExists {
|
|
log.Printf(" Test Room Existence: enabled")
|
|
}
|
|
if config.TestXEP0077 {
|
|
log.Printf(" Test XEP-0077 In-Band Registration: enabled")
|
|
}
|
|
}
|
|
|
|
// Test the XMPP client
|
|
if err := testXMPPClient(config); err != nil {
|
|
log.Fatalf("❌ XMPP client test failed: %v", err)
|
|
}
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ XMPP client test completed successfully!")
|
|
} else {
|
|
fmt.Println("✅ XMPP client connectivity test passed!")
|
|
if config.TestXEP0077 {
|
|
fmt.Println("✅ XMPP XEP-0077 In-Band Registration test passed!")
|
|
}
|
|
if config.TestMUC {
|
|
fmt.Println("✅ XMPP MUC operations test passed!")
|
|
}
|
|
if config.TestDirectMessage {
|
|
fmt.Println("✅ XMPP direct message test passed!")
|
|
}
|
|
if config.TestRoomExists {
|
|
fmt.Println("✅ XMPP room existence test passed!")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testXMPPClient(config *Config) error {
|
|
if config.Verbose {
|
|
log.Printf("Creating XMPP client...")
|
|
}
|
|
|
|
// Create a simple logger for the XMPP client
|
|
doctorLogger := &SimpleLogger{verbose: config.Verbose}
|
|
|
|
// Create XMPP client with optional TLS configuration
|
|
var client *xmpp.Client
|
|
if config.InsecureSkipVerify {
|
|
if config.Verbose {
|
|
log.Printf("Using insecure TLS configuration (skipping certificate verification)")
|
|
}
|
|
tlsConfig := &tls.Config{
|
|
InsecureSkipVerify: true, //nolint:gosec // This is a testing tool for development environments
|
|
}
|
|
client = xmpp.NewClientWithTLS(
|
|
config.Server,
|
|
config.Username,
|
|
config.Password,
|
|
config.Resource,
|
|
"doctor-remote-id",
|
|
tlsConfig,
|
|
doctorLogger,
|
|
)
|
|
} else {
|
|
client = xmpp.NewClient(
|
|
config.Server,
|
|
config.Username,
|
|
config.Password,
|
|
config.Resource,
|
|
"doctor-remote-id",
|
|
doctorLogger,
|
|
)
|
|
}
|
|
|
|
if config.Verbose {
|
|
log.Printf("Attempting to connect to XMPP server...")
|
|
}
|
|
|
|
// Test connection
|
|
start := time.Now()
|
|
err := client.Connect()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to XMPP server: %w", err)
|
|
}
|
|
connectDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Connected to XMPP server in %v", connectDuration)
|
|
log.Printf("Testing connection health...")
|
|
}
|
|
|
|
// Test connection health
|
|
start = time.Now()
|
|
err = client.Ping()
|
|
if err != nil {
|
|
return fmt.Errorf("connection health test failed: %w", err)
|
|
}
|
|
pingDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Connection health test passed in %v", pingDuration)
|
|
}
|
|
|
|
var xep0077Duration time.Duration
|
|
var mucDuration time.Duration
|
|
var dmDuration time.Duration
|
|
var roomExistsDuration time.Duration
|
|
|
|
// Test XEP-0077 In-Band Registration if requested
|
|
if config.TestXEP0077 {
|
|
start = time.Now()
|
|
err = testXEP0077(client, config)
|
|
if err != nil {
|
|
return fmt.Errorf("XEP-0077 In-Band Registration test failed: %w", err)
|
|
}
|
|
xep0077Duration = time.Since(start)
|
|
}
|
|
|
|
// Test MUC operations if requested
|
|
if config.TestMUC {
|
|
start = time.Now()
|
|
err = testMUCOperations(client, config)
|
|
if err != nil {
|
|
return fmt.Errorf("MUC operations test failed: %w", err)
|
|
}
|
|
mucDuration = time.Since(start)
|
|
}
|
|
|
|
// Test direct message if requested
|
|
if config.TestDirectMessage {
|
|
start = time.Now()
|
|
err = testDirectMessage(client, config)
|
|
if err != nil {
|
|
return fmt.Errorf("direct message test failed: %w", err)
|
|
}
|
|
dmDuration = time.Since(start)
|
|
}
|
|
|
|
// Test room existence if requested
|
|
if config.TestRoomExists {
|
|
start = time.Now()
|
|
err = testRoomExists(client, config)
|
|
if err != nil {
|
|
return fmt.Errorf("room existence test failed: %w", err)
|
|
}
|
|
roomExistsDuration = time.Since(start)
|
|
}
|
|
|
|
if config.Verbose {
|
|
log.Printf("Disconnecting from XMPP server...")
|
|
}
|
|
|
|
// Disconnect
|
|
start = time.Now()
|
|
err = client.Disconnect()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to disconnect from XMPP server: %w", err)
|
|
}
|
|
disconnectDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Disconnected from XMPP server in %v", disconnectDuration)
|
|
log.Printf("Connection summary:")
|
|
log.Printf(" Connect time: %v", connectDuration)
|
|
log.Printf(" Ping time: %v", pingDuration)
|
|
if config.TestXEP0077 {
|
|
log.Printf(" XEP-0077 test time: %v", xep0077Duration)
|
|
}
|
|
if config.TestMUC {
|
|
log.Printf(" MUC operations time: %v", mucDuration)
|
|
}
|
|
if config.TestDirectMessage {
|
|
log.Printf(" Direct message time: %v", dmDuration)
|
|
}
|
|
if config.TestRoomExists {
|
|
log.Printf(" Room existence check time: %v", roomExistsDuration)
|
|
}
|
|
log.Printf(" Disconnect time: %v", disconnectDuration)
|
|
totalTime := connectDuration + pingDuration + disconnectDuration
|
|
if config.TestXEP0077 {
|
|
totalTime += xep0077Duration
|
|
}
|
|
if config.TestMUC {
|
|
totalTime += mucDuration
|
|
}
|
|
if config.TestDirectMessage {
|
|
totalTime += dmDuration
|
|
}
|
|
if config.TestRoomExists {
|
|
totalTime += roomExistsDuration
|
|
}
|
|
log.Printf(" Total time: %v", totalTime)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func testMUCOperations(client *xmpp.Client, config *Config) error {
|
|
if config.Verbose {
|
|
log.Printf("Testing MUC operations with room: %s", config.TestRoom)
|
|
log.Printf("First checking if room exists...")
|
|
}
|
|
|
|
// Check if room exists before attempting to join
|
|
start := time.Now()
|
|
exists, err := client.CheckRoomExists(config.TestRoom)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check room existence for %s: %w", config.TestRoom, err)
|
|
}
|
|
checkDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Room existence check completed in %v", checkDuration)
|
|
log.Printf("Room %s exists: %t", config.TestRoom, exists)
|
|
}
|
|
|
|
if !exists {
|
|
return fmt.Errorf("cannot test MUC operations: room %s does not exist or is not accessible", config.TestRoom)
|
|
}
|
|
|
|
if config.Verbose {
|
|
log.Printf("Room exists, proceeding to join...")
|
|
}
|
|
|
|
// Test joining the room
|
|
start = time.Now()
|
|
err = client.JoinRoom(config.TestRoom)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to join MUC room %s: %w", config.TestRoom, err)
|
|
}
|
|
joinDuration := time.Since(start)
|
|
|
|
var sendDuration time.Duration
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Successfully joined MUC room in %v", joinDuration)
|
|
log.Printf("Sending test message to room...")
|
|
}
|
|
|
|
// Send a test message
|
|
testMessage := fmt.Sprintf("Test message from XMPP doctor at %s", time.Now().Format("15:04:05"))
|
|
messageReq := xmpp.MessageRequest{
|
|
RoomJID: config.TestRoom,
|
|
Message: testMessage,
|
|
}
|
|
|
|
start = time.Now()
|
|
_, err = client.SendMessage(&messageReq)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send test message to room %s: %w", config.TestRoom, err)
|
|
}
|
|
sendDuration = time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Successfully sent message in %v", sendDuration)
|
|
log.Printf("Message: %s", testMessage)
|
|
log.Printf("Waiting 5 seconds in the room...")
|
|
}
|
|
|
|
// Wait 5 seconds
|
|
time.Sleep(5 * time.Second)
|
|
|
|
if config.Verbose {
|
|
log.Printf("Attempting to leave MUC room...")
|
|
}
|
|
|
|
// Test leaving the room
|
|
start = time.Now()
|
|
err = client.LeaveRoom(config.TestRoom)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to leave MUC room %s: %w", config.TestRoom, err)
|
|
}
|
|
leaveDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Successfully left MUC room in %v", leaveDuration)
|
|
log.Printf("MUC operations summary:")
|
|
log.Printf(" Room existence check time: %v", checkDuration)
|
|
log.Printf(" Join time: %v", joinDuration)
|
|
log.Printf(" Send message time: %v", sendDuration)
|
|
log.Printf(" Wait time: 5s")
|
|
log.Printf(" Leave time: %v", leaveDuration)
|
|
log.Printf(" Total MUC time: %v", checkDuration+joinDuration+sendDuration+5*time.Second+leaveDuration)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func testDirectMessage(client *xmpp.Client, config *Config) error {
|
|
if config.Verbose {
|
|
log.Printf("Testing direct message functionality...")
|
|
log.Printf("Sending test message to admin user...")
|
|
}
|
|
|
|
// Send a test message to the admin user
|
|
testMessage := fmt.Sprintf("Test direct message from XMPP doctor at %s", time.Now().Format("15:04:05"))
|
|
adminJID := "admin@localhost" // Default admin user for development server
|
|
|
|
start := time.Now()
|
|
err := client.SendDirectMessage(adminJID, testMessage)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send direct message to %s: %w", adminJID, err)
|
|
}
|
|
sendDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Successfully sent direct message in %v", sendDuration)
|
|
log.Printf("Message: %s", testMessage)
|
|
log.Printf("Recipient: %s", adminJID)
|
|
log.Printf("Direct message test summary:")
|
|
log.Printf(" Send message time: %v", sendDuration)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func testRoomExists(client *xmpp.Client, config *Config) error {
|
|
if config.Verbose {
|
|
log.Printf("Testing room existence functionality...")
|
|
log.Printf("Checking if test room exists: %s", config.TestRoom)
|
|
}
|
|
|
|
// Test room existence check
|
|
start := time.Now()
|
|
exists, err := client.CheckRoomExists(config.TestRoom)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check room existence for %s: %w", config.TestRoom, err)
|
|
}
|
|
checkDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Room existence check completed in %v", checkDuration)
|
|
log.Printf("Room %s exists: %t", config.TestRoom, exists)
|
|
}
|
|
|
|
// Test with a non-existent room to verify negative case
|
|
nonExistentRoom := "nonexistent-room-12345@conference.localhost"
|
|
if config.Verbose {
|
|
log.Printf("Testing negative case with non-existent room: %s", nonExistentRoom)
|
|
}
|
|
|
|
start = time.Now()
|
|
existsNegative, err := client.CheckRoomExists(nonExistentRoom)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check non-existent room %s: %w", nonExistentRoom, err)
|
|
}
|
|
checkNegativeDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Negative room existence check completed in %v", checkNegativeDuration)
|
|
log.Printf("Non-existent room %s exists: %t (should be false)", nonExistentRoom, existsNegative)
|
|
log.Printf("Room existence test summary:")
|
|
log.Printf(" Test room check time: %v", checkDuration)
|
|
log.Printf(" Negative case check time: %v", checkNegativeDuration)
|
|
log.Printf(" Total room existence test time: %v", checkDuration+checkNegativeDuration)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func maskPassword(password string) string {
|
|
if len(password) <= 2 {
|
|
return "****"
|
|
}
|
|
return password[:2] + "****"
|
|
}
|
|
|
|
// SimpleLogger provides basic logging functionality for the doctor command
|
|
type SimpleLogger struct {
|
|
verbose bool
|
|
}
|
|
|
|
// LogDebug logs debug messages if verbose mode is enabled
|
|
func (l *SimpleLogger) LogDebug(msg string, args ...interface{}) {
|
|
if l.verbose {
|
|
log.Printf("[DEBUG] "+msg, args...)
|
|
}
|
|
}
|
|
|
|
// LogInfo logs info messages
|
|
func (l *SimpleLogger) LogInfo(msg string, args ...interface{}) {
|
|
log.Printf("[INFO] "+msg, args...)
|
|
}
|
|
|
|
// LogWarn logs warning messages
|
|
func (l *SimpleLogger) LogWarn(msg string, args ...interface{}) {
|
|
log.Printf("[WARN] "+msg, args...)
|
|
}
|
|
|
|
// LogError logs error messages
|
|
func (l *SimpleLogger) LogError(msg string, args ...interface{}) {
|
|
log.Printf("[ERROR] "+msg, args...)
|
|
}
|
|
|
|
// testXEP0077 tests XEP-0077 In-Band Registration functionality by creating and deleting a test user
|
|
func testXEP0077(client *xmpp.Client, config *Config) error {
|
|
if config.Verbose {
|
|
log.Printf("Testing XEP-0077 In-Band Registration functionality...")
|
|
}
|
|
|
|
// First, wait for server capability detection to complete
|
|
// This is handled asynchronously in the client Connect method
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Check if server supports XEP-0077
|
|
inBandReg, err := client.GetInBandRegistration()
|
|
if err != nil {
|
|
return fmt.Errorf("server does not support XEP-0077 In-Band Registration: %w", err)
|
|
}
|
|
|
|
if !inBandReg.IsEnabled() {
|
|
return fmt.Errorf("XEP-0077 In-Band Registration is not enabled on this server")
|
|
}
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Server supports XEP-0077 In-Band Registration")
|
|
}
|
|
|
|
serverJID := client.GetJID().Domain()
|
|
|
|
// Step 1: Test registration fields discovery
|
|
start := time.Now()
|
|
if config.Verbose {
|
|
log.Printf("Testing registration fields discovery for server: %s", serverJID.String())
|
|
}
|
|
|
|
fields, err := inBandReg.GetRegistrationFields(serverJID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get registration fields from server: %w", err)
|
|
}
|
|
fieldsDuration := time.Since(start)
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Registration fields discovery completed in %v", fieldsDuration)
|
|
log.Printf("Registration fields: required=%v, available=%d", fields.Required, len(fields.Fields))
|
|
}
|
|
|
|
// Step 2: Create test user
|
|
testUsername := fmt.Sprintf("xmpptest%d", time.Now().Unix())
|
|
testPassword := "testpass123"
|
|
testEmail := fmt.Sprintf("%s@localhost", testUsername)
|
|
|
|
if config.Verbose {
|
|
log.Printf("Creating test user: %s", testUsername)
|
|
}
|
|
|
|
registrationRequest := &xmpp.RegistrationRequest{
|
|
Username: testUsername,
|
|
Password: testPassword,
|
|
Email: testEmail,
|
|
}
|
|
|
|
start = time.Now()
|
|
regResponse, err := inBandReg.RegisterAccount(serverJID, registrationRequest)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to register test user '%s': %w", testUsername, err)
|
|
}
|
|
registerDuration := time.Since(start)
|
|
|
|
if !regResponse.Success {
|
|
return fmt.Errorf("user registration failed: %s", regResponse.Error)
|
|
}
|
|
|
|
if config.Verbose {
|
|
log.Printf("✅ Test user '%s' registered successfully in %v", testUsername, registerDuration)
|
|
log.Printf("Registration response: %s", regResponse.Message)
|
|
}
|
|
|
|
// Step 3: Delete test user (cleanup)
|
|
if config.Verbose {
|
|
log.Printf("Cleaning up: removing test user '%s'", testUsername)
|
|
}
|
|
|
|
start = time.Now()
|
|
cancelResponse, err := inBandReg.CancelRegistration(serverJID)
|
|
if err != nil {
|
|
if config.Verbose {
|
|
log.Printf("⚠️ Failed to remove test user '%s': %v", testUsername, err)
|
|
log.Printf("⚠️ Manual cleanup may be required")
|
|
}
|
|
} else {
|
|
cancelDuration := time.Since(start)
|
|
if cancelResponse.Success {
|
|
if config.Verbose {
|
|
log.Printf("✅ Test user '%s' removed successfully in %v", testUsername, cancelDuration)
|
|
}
|
|
} else {
|
|
if config.Verbose {
|
|
log.Printf("⚠️ User removal may have failed: %s", cancelResponse.Error)
|
|
}
|
|
}
|
|
}
|
|
|
|
if config.Verbose {
|
|
log.Printf("XEP-0077 test summary:")
|
|
log.Printf(" Server support check: ✅")
|
|
log.Printf(" Registration fields discovery time: %v", fieldsDuration)
|
|
log.Printf(" User registration time: %v", registerDuration)
|
|
log.Printf(" Test username: %s", testUsername)
|
|
log.Printf(" Required fields count: %d", len(fields.Required))
|
|
log.Printf(" User creation: ✅")
|
|
if err == nil && cancelResponse.Success {
|
|
log.Printf(" User cleanup: ✅")
|
|
} else {
|
|
log.Printf(" User cleanup: ⚠️")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|