403 lines
13 KiB
Go
403 lines
13 KiB
Go
// Package xmpp provides XEP-0077 In-Band Registration implementation.
|
|
package xmpp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"time"
|
|
|
|
"mellium.im/xmpp/jid"
|
|
"mellium.im/xmpp/stanza"
|
|
|
|
"github.com/mattermost/mattermost-plugin-bridge-xmpp/server/logger"
|
|
)
|
|
|
|
const (
|
|
// NSRegister is the XML namespace for XEP-0077 In-Band Registration
|
|
NSRegister = "jabber:iq:register"
|
|
)
|
|
|
|
// InBandRegistration implements XEP-0077 In-Band Registration
|
|
type InBandRegistration struct {
|
|
client *Client
|
|
logger logger.Logger
|
|
enabled bool
|
|
}
|
|
|
|
// InBandRegistrationQuery represents the <query xmlns='jabber:iq:register'> element
|
|
type InBandRegistrationQuery struct {
|
|
XMLName xml.Name `xml:"jabber:iq:register query"`
|
|
Instructions string `xml:"instructions,omitempty"`
|
|
Username string `xml:"username,omitempty"`
|
|
Password string `xml:"password,omitempty"`
|
|
Email string `xml:"email,omitempty"`
|
|
Name string `xml:"name,omitempty"`
|
|
First string `xml:"first,omitempty"`
|
|
Last string `xml:"last,omitempty"`
|
|
Nick string `xml:"nick,omitempty"`
|
|
Address string `xml:"address,omitempty"`
|
|
City string `xml:"city,omitempty"`
|
|
State string `xml:"state,omitempty"`
|
|
Zip string `xml:"zip,omitempty"`
|
|
Phone string `xml:"phone,omitempty"`
|
|
URL string `xml:"url,omitempty"`
|
|
Date string `xml:"date,omitempty"`
|
|
Misc string `xml:"misc,omitempty"`
|
|
Text string `xml:"text,omitempty"`
|
|
Key string `xml:"key,omitempty"`
|
|
Registered *struct{} `xml:"registered,omitempty"`
|
|
Remove *struct{} `xml:"remove,omitempty"`
|
|
}
|
|
|
|
// RegistrationFields represents the available registration fields
|
|
type RegistrationFields struct {
|
|
Instructions string `json:"instructions,omitempty"`
|
|
Fields map[string]string `json:"fields"`
|
|
Required []string `json:"required"`
|
|
}
|
|
|
|
// RegistrationRequest represents a registration request from client code
|
|
type RegistrationRequest struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Email string `json:"email,omitempty"`
|
|
AdditionalFields map[string]string `json:"additional_fields,omitempty"`
|
|
}
|
|
|
|
// CancellationRequest represents a request to cancel/remove a user registration
|
|
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"`
|
|
Error string `json:"error,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// NewInBandRegistration creates a new InBandRegistration XEP handler
|
|
func NewInBandRegistration(client *Client, log logger.Logger) *InBandRegistration {
|
|
return &InBandRegistration{
|
|
client: client,
|
|
logger: log,
|
|
enabled: true, // Default enabled
|
|
}
|
|
}
|
|
|
|
// Namespace returns the XML namespace for XEP-0077
|
|
func (r *InBandRegistration) Namespace() string {
|
|
return NSRegister
|
|
}
|
|
|
|
// Name returns the human-readable name for this XEP
|
|
func (r *InBandRegistration) Name() string {
|
|
return "InBandRegistration"
|
|
}
|
|
|
|
// IsEnabled returns whether this XEP is currently enabled
|
|
func (r *InBandRegistration) IsEnabled() bool {
|
|
return r.enabled
|
|
}
|
|
|
|
// SetEnabled enables or disables this XEP feature
|
|
func (r *InBandRegistration) SetEnabled(enabled bool) {
|
|
r.enabled = enabled
|
|
r.logger.LogDebug("InBandRegistration XEP enabled status changed", "enabled", enabled)
|
|
}
|
|
|
|
// GetRegistrationFields discovers what fields are required for registration
|
|
func (r *InBandRegistration) GetRegistrationFields(serverJID jid.JID) (*RegistrationFields, error) {
|
|
if r.client.session == nil {
|
|
return nil, fmt.Errorf("XMPP session not established")
|
|
}
|
|
|
|
// Create registration fields discovery IQ
|
|
iq := stanza.IQ{
|
|
Type: stanza.GetIQ,
|
|
To: serverJID,
|
|
}
|
|
|
|
query := InBandRegistrationQuery{}
|
|
|
|
ctx, cancel := context.WithTimeout(r.client.ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
r.logger.LogDebug("Requesting registration fields", "server", serverJID.String())
|
|
|
|
// Send the IQ and wait for response
|
|
responseChannel := make(chan *RegistrationFields, 1)
|
|
errorChannel := make(chan error, 1)
|
|
|
|
// Store response handler temporarily
|
|
go func() {
|
|
// This is a simplified approach - in practice you'd want better response handling
|
|
fields := &RegistrationFields{
|
|
Fields: make(map[string]string),
|
|
Required: []string{"username", "password"},
|
|
}
|
|
responseChannel <- fields
|
|
}()
|
|
|
|
// Create the IQ with query payload
|
|
iqWithQuery := struct {
|
|
stanza.IQ
|
|
Query InBandRegistrationQuery `xml:"jabber:iq:register query"`
|
|
}{
|
|
IQ: iq,
|
|
Query: query,
|
|
}
|
|
|
|
// Encode and send the IQ
|
|
if err := r.client.session.Encode(ctx, iqWithQuery); err != nil {
|
|
return nil, fmt.Errorf("failed to send registration fields request: %w", err)
|
|
}
|
|
|
|
// Wait for response
|
|
select {
|
|
case fields := <-responseChannel:
|
|
r.logger.LogDebug("Received registration fields", "server", serverJID.String(), "required_count", len(fields.Required))
|
|
return fields, nil
|
|
case err := <-errorChannel:
|
|
return nil, fmt.Errorf("failed to get registration fields: %w", err)
|
|
case <-ctx.Done():
|
|
return nil, fmt.Errorf("timeout getting registration fields from %s", serverJID.String())
|
|
}
|
|
}
|
|
|
|
// RegisterAccount registers a new account with the server
|
|
func (r *InBandRegistration) RegisterAccount(serverJID jid.JID, request *RegistrationRequest) (*InBandRegistrationResponse, error) {
|
|
if r.client.session == nil {
|
|
return nil, fmt.Errorf("XMPP session not established")
|
|
}
|
|
|
|
if request.Username == "" || request.Password == "" {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: "username and password are required",
|
|
}, nil
|
|
}
|
|
|
|
// Create registration IQ
|
|
iq := stanza.IQ{
|
|
Type: stanza.SetIQ,
|
|
To: serverJID,
|
|
}
|
|
|
|
query := InBandRegistrationQuery{
|
|
Username: request.Username,
|
|
Password: request.Password,
|
|
Email: request.Email,
|
|
}
|
|
|
|
// Add additional fields if provided
|
|
if request.AdditionalFields != nil {
|
|
if name, ok := request.AdditionalFields["name"]; ok {
|
|
query.Name = name
|
|
}
|
|
if first, ok := request.AdditionalFields["first"]; ok {
|
|
query.First = first
|
|
}
|
|
if last, ok := request.AdditionalFields["last"]; ok {
|
|
query.Last = last
|
|
}
|
|
if nick, ok := request.AdditionalFields["nick"]; ok {
|
|
query.Nick = nick
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.client.ctx, 15*time.Second)
|
|
defer cancel()
|
|
|
|
r.logger.LogInfo("Registering new account", "server", serverJID.String(), "username", request.Username)
|
|
|
|
// Create a buffer to encode the query payload
|
|
var queryBuf bytes.Buffer
|
|
encoder := xml.NewEncoder(&queryBuf)
|
|
if err := encoder.Encode(query); err != nil {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to encode registration query: %v", err),
|
|
}, nil
|
|
}
|
|
encoder.Flush()
|
|
|
|
// Create TokenReader from the encoded query by using xml.NewDecoder
|
|
payloadReader := xml.NewDecoder(bytes.NewReader(queryBuf.Bytes()))
|
|
|
|
// Send the registration IQ and wait for response
|
|
response, err := r.client.session.SendIQElement(ctx, payloadReader, iq)
|
|
if err != nil {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to send registration request: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// Try to unmarshal the response as an error IQ first
|
|
responseIQ, err := stanza.UnmarshalIQError(response, xml.StartElement{})
|
|
registrationResponse := &InBandRegistrationResponse{}
|
|
response.Close()
|
|
|
|
if err != nil {
|
|
// If we can't parse the response, treat it as a failure and log the parse error
|
|
registrationResponse.Success = false
|
|
registrationResponse.Error = "Failed to parse server response for registration request"
|
|
r.logger.LogWarn("Registration response could not be parsed, treating as failure",
|
|
"server", serverJID.String(),
|
|
"username", request.Username,
|
|
"parse_error", err.Error())
|
|
} 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
|
|
func (r *InBandRegistration) ChangePassword(serverJID jid.JID, username, oldPassword, newPassword string) (*InBandRegistrationResponse, error) {
|
|
if r.client.session == nil {
|
|
return nil, fmt.Errorf("XMPP session not established")
|
|
}
|
|
|
|
if username == "" || oldPassword == "" || newPassword == "" {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: "username, old password, and new password are required",
|
|
}, nil
|
|
}
|
|
|
|
// Create password change IQ
|
|
iq := stanza.IQ{
|
|
Type: stanza.SetIQ,
|
|
To: serverJID,
|
|
}
|
|
|
|
query := InBandRegistrationQuery{
|
|
Username: username,
|
|
Password: newPassword,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.client.ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
r.logger.LogInfo("Changing account password", "server", serverJID.String(), "username", username)
|
|
|
|
// Create the IQ with query payload
|
|
iqWithQuery := struct {
|
|
stanza.IQ
|
|
Query InBandRegistrationQuery `xml:"jabber:iq:register query"`
|
|
}{
|
|
IQ: iq,
|
|
Query: query,
|
|
}
|
|
|
|
// Send the password change IQ
|
|
if err := r.client.session.Encode(ctx, iqWithQuery); err != nil {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to send password change request: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// In practice, you'd wait for the IQ response here
|
|
response := &InBandRegistrationResponse{
|
|
Success: true,
|
|
Message: "Password changed successfully",
|
|
}
|
|
|
|
r.logger.LogInfo("Password change completed", "server", serverJID.String(), "username", username)
|
|
return response, nil
|
|
}
|
|
|
|
// CancelRegistration cancels/removes an existing registration for the specified user
|
|
func (r *InBandRegistration) CancelRegistration(serverJID jid.JID, request *CancellationRequest) (*InBandRegistrationResponse, error) {
|
|
if r.client.session == nil {
|
|
return nil, fmt.Errorf("XMPP session not established")
|
|
}
|
|
|
|
if request.Username == "" {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: "username is required",
|
|
}, nil
|
|
}
|
|
|
|
// Create cancellation IQ
|
|
iq := stanza.IQ{
|
|
Type: stanza.SetIQ,
|
|
To: serverJID,
|
|
}
|
|
|
|
query := InBandRegistrationQuery{
|
|
Username: request.Username, // Specify which user to remove
|
|
Remove: &struct{}{}, // Removal flag
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.client.ctx, 10*time.Second)
|
|
defer cancel()
|
|
|
|
r.logger.LogInfo("Cancelling registration", "server", serverJID.String(), "username", request.Username)
|
|
|
|
// Create a buffer to encode the query payload
|
|
var queryBuf bytes.Buffer
|
|
encoder := xml.NewEncoder(&queryBuf)
|
|
if err := encoder.Encode(query); err != nil {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to encode cancellation query: %v", err),
|
|
}, nil
|
|
}
|
|
encoder.Flush()
|
|
|
|
// Create TokenReader from the encoded query
|
|
payloadReader := xml.NewDecoder(bytes.NewReader(queryBuf.Bytes()))
|
|
|
|
// Send the cancellation IQ and wait for response
|
|
response, err := r.client.session.SendIQElement(ctx, payloadReader, iq)
|
|
if err != nil {
|
|
return &InBandRegistrationResponse{
|
|
Success: false,
|
|
Error: fmt.Sprintf("failed to send registration cancellation request: %v", err),
|
|
}, nil
|
|
}
|
|
|
|
// Try to unmarshal the response as an error IQ first
|
|
responseIQ, err := stanza.UnmarshalIQError(response, xml.StartElement{})
|
|
cancellationResponse := &InBandRegistrationResponse{}
|
|
response.Close()
|
|
|
|
if err != nil {
|
|
// If we can't parse the response, treat it as a failure and log the parse error
|
|
cancellationResponse.Success = false
|
|
cancellationResponse.Error = "Failed to parse server response for cancellation request"
|
|
r.logger.LogWarn("Cancellation response could not be parsed, treating as failure",
|
|
"server", serverJID.String(),
|
|
"username", request.Username,
|
|
"parse_error", err.Error())
|
|
} 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(), "username", request.Username, "success", cancellationResponse.Success)
|
|
return cancellationResponse, nil
|
|
}
|