fix: properly send inbandregistration requests

This commit is contained in:
Felipe M 2025-08-12 18:43:21 +02:00
parent 4e4a290813
commit b99b412692
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
3 changed files with 117 additions and 114 deletions

View file

@ -367,47 +367,3 @@ func (u *User) GetJID() string {
func (u *User) GetClient() *xmppClient.Client { func (u *User) GetClient() *xmppClient.Client {
return u.client return u.client
} }
// UpdateCredentials updates the user's JID and password for ghost user mode
// This creates a new XMPP client with the updated credentials
func (u *User) UpdateCredentials(newJID, newPassword string) error {
u.logger.LogDebug("Updating XMPP user credentials", "user_id", u.id, "old_jid", u.jid, "new_jid", newJID)
// Disconnect existing client if connected
wasConnected := u.IsConnected()
if wasConnected {
if err := u.Disconnect(); err != nil {
u.logger.LogWarn("Error disconnecting before credential update", "user_id", u.id, "error", err)
}
}
// Create TLS config based on certificate verification setting
tlsConfig := &tls.Config{
InsecureSkipVerify: u.config.XMPPInsecureSkipVerify, //nolint:gosec // Allow insecure TLS for testing environments
}
// Create new XMPP client with updated credentials
newClient := xmppClient.NewClientWithTLS(
u.config.XMPPServerURL,
newJID,
newPassword,
u.config.GetXMPPResource(),
u.id, // Use user ID as remote ID
tlsConfig,
u.logger,
)
// Update user fields
u.jid = newJID
u.client = newClient
// Reconnect if we were previously connected
if wasConnected {
if err := u.Connect(); err != nil {
return fmt.Errorf("failed to reconnect after credential update: %w", err)
}
}
u.logger.LogInfo("XMPP user credentials updated successfully", "user_id", u.id, "new_jid", newJID)
return nil
}

View file

@ -396,7 +396,10 @@ func (m *UserManager) cleanupGhostUser(mattermostUserID string) error {
} }
// Unregister the ghost user account via XEP-0077 // Unregister the ghost user account via XEP-0077
response, err := regHandler.CancelRegistration(ghostJIDParsed.Domain()) cancellationRequest := &xmppClient.CancellationRequest{
Username: ghostJIDParsed.Localpart(), // Extract username from ghost JID
}
response, err := regHandler.CancelRegistration(ghostJIDParsed.Domain(), cancellationRequest)
if err != nil { if err != nil {
return fmt.Errorf("failed to cancel registration for ghost user %s: %w", ghostData.GhostJID, err) return fmt.Errorf("failed to cancel registration for ghost user %s: %w", ghostData.GhostJID, err)
} }

View file

@ -2,6 +2,7 @@
package xmpp package xmpp
import ( import (
"bytes"
"context" "context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
@ -25,8 +26,8 @@ type InBandRegistration struct {
enabled bool enabled bool
} }
// RegistrationQuery represents the <query xmlns='jabber:iq:register'> element // InBandRegistrationQuery represents the <query xmlns='jabber:iq:register'> element
type RegistrationQuery struct { type InBandRegistrationQuery struct {
XMLName xml.Name `xml:"jabber:iq:register query"` XMLName xml.Name `xml:"jabber:iq:register query"`
Instructions string `xml:"instructions,omitempty"` Instructions string `xml:"instructions,omitempty"`
Username string `xml:"username,omitempty"` Username string `xml:"username,omitempty"`
@ -65,8 +66,13 @@ type RegistrationRequest struct {
AdditionalFields map[string]string `json:"additional_fields,omitempty"` AdditionalFields map[string]string `json:"additional_fields,omitempty"`
} }
// RegistrationResponse represents the result of a registration operation // CancellationRequest represents a request to cancel/remove a user registration
type RegistrationResponse struct { type CancellationRequest struct {
Username string `json:"username"`
}
// InBandRegistrationResponse represents the result of any XEP-0077 In-Band Registration operation
type InBandRegistrationResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
@ -114,7 +120,7 @@ func (r *InBandRegistration) GetRegistrationFields(serverJID jid.JID) (*Registra
To: serverJID, To: serverJID,
} }
query := RegistrationQuery{} query := InBandRegistrationQuery{}
ctx, cancel := context.WithTimeout(r.client.ctx, 10*time.Second) ctx, cancel := context.WithTimeout(r.client.ctx, 10*time.Second)
defer cancel() defer cancel()
@ -138,7 +144,7 @@ func (r *InBandRegistration) GetRegistrationFields(serverJID jid.JID) (*Registra
// Create the IQ with query payload // Create the IQ with query payload
iqWithQuery := struct { iqWithQuery := struct {
stanza.IQ stanza.IQ
Query RegistrationQuery `xml:"jabber:iq:register query"` Query InBandRegistrationQuery `xml:"jabber:iq:register query"`
}{ }{
IQ: iq, IQ: iq,
Query: query, Query: query,
@ -162,13 +168,13 @@ func (r *InBandRegistration) GetRegistrationFields(serverJID jid.JID) (*Registra
} }
// RegisterAccount registers a new account with the server // RegisterAccount registers a new account with the server
func (r *InBandRegistration) RegisterAccount(serverJID jid.JID, request *RegistrationRequest) (*RegistrationResponse, error) { func (r *InBandRegistration) RegisterAccount(serverJID jid.JID, request *RegistrationRequest) (*InBandRegistrationResponse, error) {
if r.client.session == nil { if r.client.session == nil {
return nil, fmt.Errorf("XMPP session not established") return nil, fmt.Errorf("XMPP session not established")
} }
if request.Username == "" || request.Password == "" { if request.Username == "" || request.Password == "" {
return &RegistrationResponse{ return &InBandRegistrationResponse{
Success: false, Success: false,
Error: "username and password are required", Error: "username and password are required",
}, nil }, nil
@ -180,7 +186,7 @@ func (r *InBandRegistration) RegisterAccount(serverJID jid.JID, request *Registr
To: serverJID, To: serverJID,
} }
query := RegistrationQuery{ query := InBandRegistrationQuery{
Username: request.Username, Username: request.Username,
Password: request.Password, Password: request.Password,
Email: request.Email, Email: request.Email,
@ -207,57 +213,64 @@ func (r *InBandRegistration) RegisterAccount(serverJID jid.JID, request *Registr
r.logger.LogInfo("Registering new account", "server", serverJID.String(), "username", request.Username) r.logger.LogInfo("Registering new account", "server", serverJID.String(), "username", request.Username)
// Create response channels // Create a buffer to encode the query payload
responseChannel := make(chan *RegistrationResponse, 1) var queryBuf bytes.Buffer
encoder := xml.NewEncoder(&queryBuf)
// Store response handler temporarily if err := encoder.Encode(query); err != nil {
go func() { return &InBandRegistrationResponse{
// This is a simplified approach - in practice you'd want proper IQ response handling Success: false,
response := &RegistrationResponse{ Error: fmt.Sprintf("failed to encode registration query: %v", err),
Success: true, }, nil
Message: "Account registered successfully",
}
responseChannel <- response
}()
// Create the IQ with query payload
iqWithQuery := struct {
stanza.IQ
Query RegistrationQuery `xml:"jabber:iq:register query"`
}{
IQ: iq,
Query: query,
} }
encoder.Flush()
// Encode and send the registration IQ // Create TokenReader from the encoded query by using xml.NewDecoder
if err := r.client.session.Encode(ctx, iqWithQuery); err != nil { payloadReader := xml.NewDecoder(bytes.NewReader(queryBuf.Bytes()))
return &RegistrationResponse{
// Send the registration IQ and wait for response
response, err := r.client.session.SendIQElement(ctx, payloadReader, iq)
if err != nil {
return &InBandRegistrationResponse{
Success: false, Success: false,
Error: fmt.Sprintf("failed to send registration request: %v", err), Error: fmt.Sprintf("failed to send registration request: %v", err),
}, nil }, nil
} }
defer response.Close()
// Wait for response // Try to unmarshal the response as an error IQ first
select { responseIQ, err := stanza.UnmarshalIQError(response, xml.StartElement{})
case response := <-responseChannel: registrationResponse := &InBandRegistrationResponse{}
r.logger.LogInfo("Account registration completed", "server", serverJID.String(), "username", request.Username, "success", response.Success)
return response, nil if err != nil {
case <-ctx.Done(): // If we can't unmarshal as error IQ, check if it's a success response
return &RegistrationResponse{ // For now, assume success if no error occurred during sending
Success: false, registrationResponse.Success = true
Error: fmt.Sprintf("timeout registering account with %s", serverJID.String()), registrationResponse.Message = "Account registration completed successfully"
}, nil r.logger.LogDebug("Registration response could not be parsed as error IQ, assuming success", "server", serverJID.String(), "username", request.Username, "error", err)
} else {
// Successfully unmarshaled - check IQ type
if responseIQ.Type == stanza.ErrorIQ {
registrationResponse.Success = false
registrationResponse.Error = "Server returned error for registration request"
r.logger.LogWarn("Registration failed with server error", "server", serverJID.String(), "username", request.Username, "iq_type", responseIQ.Type)
} else {
registrationResponse.Success = true
registrationResponse.Message = "Account registration completed successfully"
}
} }
r.logger.LogInfo("Account registration completed", "server", serverJID.String(), "username", request.Username, "success", registrationResponse.Success)
return registrationResponse, nil
} }
// ChangePassword changes the password for an existing account // ChangePassword changes the password for an existing account
func (r *InBandRegistration) ChangePassword(serverJID jid.JID, username, oldPassword, newPassword string) (*RegistrationResponse, error) { func (r *InBandRegistration) ChangePassword(serverJID jid.JID, username, oldPassword, newPassword string) (*InBandRegistrationResponse, error) {
if r.client.session == nil { if r.client.session == nil {
return nil, fmt.Errorf("XMPP session not established") return nil, fmt.Errorf("XMPP session not established")
} }
if username == "" || oldPassword == "" || newPassword == "" { if username == "" || oldPassword == "" || newPassword == "" {
return &RegistrationResponse{ return &InBandRegistrationResponse{
Success: false, Success: false,
Error: "username, old password, and new password are required", Error: "username, old password, and new password are required",
}, nil }, nil
@ -269,7 +282,7 @@ func (r *InBandRegistration) ChangePassword(serverJID jid.JID, username, oldPass
To: serverJID, To: serverJID,
} }
query := RegistrationQuery{ query := InBandRegistrationQuery{
Username: username, Username: username,
Password: newPassword, Password: newPassword,
} }
@ -282,7 +295,7 @@ func (r *InBandRegistration) ChangePassword(serverJID jid.JID, username, oldPass
// Create the IQ with query payload // Create the IQ with query payload
iqWithQuery := struct { iqWithQuery := struct {
stanza.IQ stanza.IQ
Query RegistrationQuery `xml:"jabber:iq:register query"` Query InBandRegistrationQuery `xml:"jabber:iq:register query"`
}{ }{
IQ: iq, IQ: iq,
Query: query, Query: query,
@ -290,14 +303,14 @@ func (r *InBandRegistration) ChangePassword(serverJID jid.JID, username, oldPass
// Send the password change IQ // Send the password change IQ
if err := r.client.session.Encode(ctx, iqWithQuery); err != nil { if err := r.client.session.Encode(ctx, iqWithQuery); err != nil {
return &RegistrationResponse{ return &InBandRegistrationResponse{
Success: false, Success: false,
Error: fmt.Sprintf("failed to send password change request: %v", err), Error: fmt.Sprintf("failed to send password change request: %v", err),
}, nil }, nil
} }
// In practice, you'd wait for the IQ response here // In practice, you'd wait for the IQ response here
response := &RegistrationResponse{ response := &InBandRegistrationResponse{
Success: true, Success: true,
Message: "Password changed successfully", Message: "Password changed successfully",
} }
@ -306,50 +319,81 @@ func (r *InBandRegistration) ChangePassword(serverJID jid.JID, username, oldPass
return response, nil return response, nil
} }
// CancelRegistration cancels/removes an existing registration // CancelRegistration cancels/removes an existing registration for the specified user
func (r *InBandRegistration) CancelRegistration(serverJID jid.JID) (*RegistrationResponse, error) { func (r *InBandRegistration) CancelRegistration(serverJID jid.JID, request *CancellationRequest) (*InBandRegistrationResponse, error) {
if r.client.session == nil { if r.client.session == nil {
return nil, fmt.Errorf("XMPP session not established") return nil, fmt.Errorf("XMPP session not established")
} }
if request.Username == "" {
return &InBandRegistrationResponse{
Success: false,
Error: "username is required",
}, nil
}
// Create cancellation IQ // Create cancellation IQ
iq := stanza.IQ{ iq := stanza.IQ{
Type: stanza.SetIQ, Type: stanza.SetIQ,
To: serverJID, To: serverJID,
} }
query := RegistrationQuery{ query := InBandRegistrationQuery{
Remove: &struct{}{}, // Empty struct indicates removal Username: request.Username, // Specify which user to remove
Remove: &struct{}{}, // Removal flag
} }
ctx, cancel := context.WithTimeout(r.client.ctx, 10*time.Second) ctx, cancel := context.WithTimeout(r.client.ctx, 10*time.Second)
defer cancel() defer cancel()
r.logger.LogInfo("Cancelling registration", "server", serverJID.String()) r.logger.LogInfo("Cancelling registration", "server", serverJID.String(), "username", request.Username)
// Create the IQ with query payload // Create a buffer to encode the query payload
iqWithQuery := struct { var queryBuf bytes.Buffer
stanza.IQ encoder := xml.NewEncoder(&queryBuf)
Query RegistrationQuery `xml:"jabber:iq:register query"` if err := encoder.Encode(query); err != nil {
}{ return &InBandRegistrationResponse{
IQ: iq, Success: false,
Query: query, Error: fmt.Sprintf("failed to encode cancellation query: %v", err),
}, nil
} }
encoder.Flush()
// Send the cancellation IQ // Create TokenReader from the encoded query
if err := r.client.session.Encode(ctx, iqWithQuery); err != nil { payloadReader := xml.NewDecoder(bytes.NewReader(queryBuf.Bytes()))
return &RegistrationResponse{
// Send the cancellation IQ and wait for response
response, err := r.client.session.SendIQElement(ctx, payloadReader, iq)
if err != nil {
return &InBandRegistrationResponse{
Success: false, Success: false,
Error: fmt.Sprintf("failed to send registration cancellation request: %v", err), Error: fmt.Sprintf("failed to send registration cancellation request: %v", err),
}, nil }, nil
} }
defer response.Close()
// In practice, you'd wait for the IQ response here // Try to unmarshal the response as an error IQ first
response := &RegistrationResponse{ responseIQ, err := stanza.UnmarshalIQError(response, xml.StartElement{})
Success: true, cancellationResponse := &InBandRegistrationResponse{}
Message: "Registration cancelled successfully",
if err != nil {
// If we can't unmarshal as error IQ, check if it's a success response
// For now, assume success if no error occurred during sending
cancellationResponse.Success = true
cancellationResponse.Message = "Registration cancelled successfully"
r.logger.LogDebug("Cancellation response could not be parsed as error IQ, assuming success", "server", serverJID.String(), "username", request.Username, "error", err)
} else {
// Successfully unmarshaled - check IQ type
if responseIQ.Type == stanza.ErrorIQ {
cancellationResponse.Success = false
cancellationResponse.Error = "Server returned error for cancellation request"
r.logger.LogWarn("Registration cancellation failed with server error", "server", serverJID.String(), "username", request.Username, "iq_type", responseIQ.Type)
} else {
cancellationResponse.Success = true
cancellationResponse.Message = "Registration cancelled successfully"
}
} }
r.logger.LogInfo("Registration cancellation completed", "server", serverJID.String()) r.logger.LogInfo("Registration cancellation completed", "server", serverJID.String(), "username", request.Username, "success", cancellationResponse.Success)
return response, nil return cancellationResponse, nil
} }