/*
* Copyright 2006-2010 Daniel Henninger. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package net.sf.kraken.registration;
import java.util.Collection;
import java.util.List;
import net.sf.kraken.BaseTransport;
import net.sf.kraken.session.TransportSession;
import net.sf.kraken.type.ConnectionFailureReason;
import net.sf.kraken.type.NameSpace;
import net.sf.kraken.type.PresenceType;
import org.apache.log4j.Logger;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.ChannelHandler;
import org.jivesoftware.openfire.PacketException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException;
import org.xmpp.forms.DataForm;
import org.xmpp.forms.FormField;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Presence;
import org.xmpp.packet.PacketError.Condition;
/**
* Handles IQ-register stanzas (as defined by XEP-077) that are used to register
* or deregister a particular XMPP entity from/to a gateway.
*
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
* @author Daniel Henninger
* @see <a
* href="http://xmpp.org/extensions/xep-0077.html">XEP-077: In-Band Registration</a>
*/
public class RegistrationHandler implements ChannelHandler<IQ> {
static Logger Log = Logger.getLogger(RegistrationHandler.class);
private final BaseTransport parent;
/**
* Creates a new RegistrationHandler that can service the transport provided
* in the first argument.
*
* @param parent The transport that is serviced by the new instance.
*/
public RegistrationHandler(BaseTransport parent) {
if (parent == null) {
throw new IllegalArgumentException(
"Argument 'parent' cannot be null.");
}
this.parent = parent;
}
/*
* (non-Javadoc)
*
* @see
* org.jivesoftware.openfire.ChannelHandler#process(org.xmpp.packet.Packet)
*/
public void process(IQ packet) throws UnauthorizedException,
PacketException {
// sanitize the input
if (packet == null) {
throw new IllegalArgumentException(
"Argument 'packet' cannot be null.");
}
final String xmlns;
final Element child = (packet).getChildElement();
if (child != null) {
xmlns = child.getNamespaceURI();
}
else {
xmlns = null;
}
if (xmlns == null) {
// No namespace defined.
Log.debug("Cannot process this stanza, as it has no namespace:"
+ packet.toXML());
final IQ error = IQ.createResultIQ(packet);
error.setError(Condition.bad_request);
parent.sendPacket(error);
return;
}
// done sanitizing, start processing.
final Element remove = packet.getChildElement().element("remove");
if (remove != null) {
// User wants to unregister. =(
// this.convinceNotToLeave() ... kidding.
handleDeregister(packet);
}
else {
// handle the request
switch (packet.getType()) {
case get:
// client requests registration form
getRegistrationForm(packet);
break;
case set:
// client is providing (filled out) registration form
setRegistrationForm(packet);
break;
default:
// ignore result and error stanzas.
break;
}
}
}
/**
* Processes an IQ-register request that is expressing the wish to
* deregister from a gateway.
*
* @param packet the IQ-register stanza.
*/
private void handleDeregister(final IQ packet) {
final IQ result = IQ.createResultIQ(packet);
if (packet.getChildElement().elements().size() != 1) {
Log.debug("Cannot process this stanza - exactly one"
+ " childelement of <remove> expected:" + packet.toXML());
final IQ error = IQ.createResultIQ(packet);
error.setError(Condition.bad_request);
parent.sendPacket(error);
return;
}
final JID from = packet.getFrom();
final JID to = packet.getTo();
// Tell the end user the transport went byebye.
final Presence unavailable = new Presence(Presence.Type.unavailable);
unavailable.setTo(from);
unavailable.setFrom(to);
this.parent.sendPacket(unavailable);
try {
deleteRegistration(from);
}
catch (UserNotFoundException e) {
Log.debug("Error cleaning up contact list of: " + from);
result.setError(Condition.registration_required);
}
parent.sendPacket(result);
}
/**
* Handles a IQ-register 'get' request, which is to be interpreted as a
* request for a registration form template. The template will be prefilled
* with data, if the requestee has a current registration with the gateway.
*
* @param packet the IQ-register 'get' stanza.
* @throws UnauthorizedException if the user is not allowed to make use of the gateway.
*/
private void getRegistrationForm(IQ packet) throws UnauthorizedException {
final JID from = packet.getFrom();
final IQ result = IQ.createResultIQ(packet);
// search for existing registrations
String curUsername = null;
String curPassword = null;
String curNickname = null;
Boolean registered = false;
final Collection<Registration> registrations = RegistrationManager
.getInstance().getRegistrations(from, parent.transportType);
if (registrations.iterator().hasNext()) {
Registration registration = registrations.iterator().next();
curUsername = registration.getUsername();
curPassword = registration.getPassword();
curNickname = registration.getNickname();
registered = true;
}
// Verify that the user is allowed to make use of the gateway.
if (!registered && !parent.permissionManager.hasAccess(from)) {
// User does not have permission to register with transport.
// We want to allow them to change settings if they are already
// registered.
throw new UnauthorizedException(LocaleUtils.getLocalizedString(
"gateway.base.registrationdeniedbyacls", "kraken"));
}
// generate a template registration form.
final Element response = DocumentHelper.createElement(QName.get(
"query", NameSpace.IQ_REGISTER));
final DataForm form = new DataForm(DataForm.Type.form);
form.addInstruction(parent.getTerminologyRegistration());
final FormField usernameField = form.addField();
usernameField.setLabel(parent.getTerminologyUsername());
usernameField.setVariable("username");
usernameField.setType(FormField.Type.text_single);
if (curUsername != null) {
usernameField.addValue(curUsername);
}
final FormField passwordField = form.addField();
passwordField.setLabel(parent.getTerminologyPassword());
passwordField.setVariable("password");
passwordField.setType(FormField.Type.text_private);
if (curPassword != null) {
passwordField.addValue(curPassword);
}
final String nicknameTerm = parent.getTerminologyNickname();
if (nicknameTerm != null) {
FormField nicknameField = form.addField();
nicknameField.setLabel(nicknameTerm);
nicknameField.setVariable("nick");
nicknameField.setType(FormField.Type.text_single);
if (curNickname != null) {
nicknameField.addValue(curNickname);
}
}
response.add(form.getElement());
response.addElement("instructions").addText(
parent.getTerminologyRegistration());
// prefill the template with existing data if a registration already
// exists.
if (registered) {
response.addElement("registered");
response.addElement("username").addText(curUsername);
if (curPassword == null) {
response.addElement("password");
}
else {
response.addElement("password").addText(curPassword);
}
if (nicknameTerm != null) {
if (curNickname == null) {
response.addElement("nick");
}
else {
response.addElement("nick").addText(curNickname);
}
}
}
else {
response.addElement("username");
response.addElement("password");
if (nicknameTerm != null) {
response.addElement("nick");
}
}
// Add special indicator for rosterless gateway handling.
response.addElement( QName.get( "x", NameSpace.IQ_GATEWAY_REGISTER) );
result.setChildElement(response);
parent.sendPacket(result);
}
/**
* Handles a IQ-register 'set' request, which is to be interpreted as a
* request to create a new registration.
*
* @param packet the IQ-register 'set' stanza.
* @throws UnauthorizedException if the user isn't allowed to register.
*/
private void setRegistrationForm(IQ packet) throws UnauthorizedException {
final JID from = packet.getFrom();
final boolean registered;
Collection<Registration> registrations = RegistrationManager.getInstance().getRegistrations(from, parent.transportType);
if (registrations.iterator().hasNext()) {
registered = true;
}
else {
registered = false;
}
if (!registered && !parent.permissionManager.hasAccess(from)) {
// User does not have permission to register with transport.
// We want to allow them to change settings if they are already
// registered.
throw new UnauthorizedException(LocaleUtils.getLocalizedString(
"gateway.base.registrationdeniedbyacls", "kraken"));
}
// Parse the input variables
String username = null;
String password = null;
String nickname = null;
try {
if (packet.getChildElement().element("x") != null) {
final DataForm form = new DataForm(packet.getChildElement()
.element("x"));
final List<FormField> fields = form.getFields();
for (final FormField field : fields) {
final String var = field.getVariable();
if (var.equals("username")) {
username = field.getValues().get(0);
}
else if (var.equals("password")) {
password = field.getValues().get(0);
}
else if (var.equals("nick")) {
nickname = field.getValues().get(0);
}
}
}
}
// TODO: This shouldn't be done by catching an Exception - check for the
// existence of elements instead. If we insist doing this with an
// exception handler, prevent catching a generic Exception (catch more
// specific subclasses instead).
catch (Exception ex) {
// No with data form apparently
Log.info("Most likely, no dataform was present "
+ "in the IQ-register request.", ex);
}
// input variables could also exist in the non-extended elements
final Element userEl = packet.getChildElement().element("username");
final Element passEl = packet.getChildElement().element("password");
final Element nickEl = packet.getChildElement().element("nick");
if (userEl != null) {
username = userEl.getTextTrim();
}
if (passEl != null) {
password = passEl.getTextTrim();
}
if (nickEl != null) {
nickname = nickEl.getTextTrim();
}
username = (username == null || username.equals("")) ? null : username;
password = (password == null || password.equals("")) ? null : password;
nickname = (nickname == null || nickname.equals("")) ? null : nickname;
// verify that we've got wat we need.
if (username == null
|| (parent.isPasswordRequired() && password == null)
|| (parent.isNicknameRequired() && nickname == null)) {
// Invalid information from stanza, lets yell.
Log.info("Cannot process IQ register request, as it "
+ "fails to provide all data that's required: "
+ packet.toXML());
final IQ result = IQ.createResultIQ(packet);
result.setError(Condition.bad_request);
parent.sendPacket(result);
return;
}
// Check if the client supports our proprietary 'rosterless' mode.
final boolean rosterlessMode;
final Element x = packet.getChildElement().element("x");
if (x != null && x.getNamespaceURI() != null
&& x.getNamespaceURI().equals(NameSpace.IQ_GATEWAY_REGISTER)) {
rosterlessMode = true;
Log.info("Registering " + packet.getFrom() + " as " + username
+ " in rosterless mode.");
}
else {
rosterlessMode = false;
Log.info("Registering " + packet.getFrom() + " as " + username
+ " (without making use of rosterless mode).");
}
// Here's where the true magic lies: create the registration!
try {
addNewRegistration(from, username, password, nickname, rosterlessMode);
registrations = RegistrationManager.getInstance().getRegistrations(from, parent.transportType);
Registration registration = registrations.iterator().next();
TransportSession session = parent.registrationLoggedIn(registration, from, PresenceType.available, "", -1);
session.setRegistrationPacket(packet);
session.detachSession();
parent.getSessionManager().storeSession(from, session);
//final IQ result = IQ.createResultIQ(packet);
// I believe this shouldn't be included. Leaving it around just in
// case.
// Element response =
// DocumentHelper.createElement(QName.get("query", IQ_REGISTER));
// result.setChildElement(response);
//parent.sendPacket(result);
}
catch (UserNotFoundException e) {
Log.warn("Someone attempted to register with the gateway "
+ "who is not registered with the server: " + from);
final IQ eresult = IQ.createResultIQ(packet);
eresult.setError(Condition.forbidden);
parent.sendPacket(eresult);
final Message em = new Message();
em.setType(Message.Type.error);
em.setTo(packet.getFrom());
em.setFrom(packet.getTo());
em.setBody(LocaleUtils.getLocalizedString(
"gateway.base.registrationdeniednoacct", "kraken"));
parent.sendPacket(em);
}
catch (IllegalAccessException e) {
Log.warn("Someone who is not a user of this server "
+ "tried to register with the transport: " + from);
final IQ eresult = IQ.createResultIQ(packet);
eresult.setError(Condition.forbidden);
parent.sendPacket(eresult);
final Message em = new Message();
em.setType(Message.Type.error);
em.setTo(packet.getFrom());
em.setFrom(packet.getTo());
em.setBody(LocaleUtils.getLocalizedString(
"gateway.base.registrationdeniedbyhost", "kraken"));
parent.sendPacket(em);
}
catch (IllegalArgumentException e) {
Log.warn("Someone attempted to register with the "
+ "gateway with an invalid username: " + from);
final IQ eresult = IQ.createResultIQ(packet);
eresult.setError(Condition.bad_request);
parent.sendPacket(eresult);
final Message em = new Message();
em.setType(Message.Type.error);
em.setTo(packet.getFrom());
em.setFrom(packet.getTo());
em.setBody(LocaleUtils.getLocalizedString(
"gateway.base.registrationdeniedbadusername", "kraken"));
parent.sendPacket(em);
}
}
public void completeRegistration(TransportSession session) {
final IQ result = IQ.createResultIQ(session.getRegistrationPacket());
if (!session.getFailureStatus().equals(ConnectionFailureReason.NO_ISSUE)) {
// Ooh there was a connection issue, we're going to report that back!
if (session.getFailureStatus().equals(ConnectionFailureReason.USERNAME_OR_PASSWORD_INCORRECT)) {
result.setError(Condition.not_authorized);
}
else if (session.getFailureStatus().equals(ConnectionFailureReason.CAN_NOT_CONNECT)) {
result.setError(Condition.service_unavailable);
}
else if (session.getFailureStatus().equals(ConnectionFailureReason.LOCKED_OUT)) {
result.setError(Condition.forbidden);
}
else {
result.setError(Condition.undefined_condition);
}
result.setType(IQ.Type.error);
}
parent.sendPacket(result);
session.setRegistrationPacket(null);
// Lets ask them what their presence is, maybe log them in immediately.
final Presence p = new Presence(Presence.Type.probe);
p.setTo(session.getJID());
p.setFrom(parent.getJID());
parent.sendPacket(p);
}
/**
* Adds a registration with this transport, or updates an existing one.
*
* @param jid JID of user to add registration to.
* @param username Legacy username of registration.
* @param password Legacy password of registration.
* @param nickname Legacy nickname of registration.
* @param noRosterItem True if the transport is not to show up in the user's roster.
* @throws UserNotFoundException if registration or roster not found.
* @throws IllegalAccessException if jid is not from this server.
* @throws IllegalArgumentException if username is not valid for this transport type.
*/
public void addNewRegistration(JID jid, String username, String password,
String nickname, Boolean noRosterItem)
throws UserNotFoundException, IllegalAccessException {
Log.debug("Adding or updating registration for : " + jid.toString()
+ " / " + username);
if (!XMPPServer.getInstance().isLocal(jid)) {
throw new IllegalAccessException(
"Domain of jid registering does not match domain of server.");
}
if (!parent.isUsernameValid(username)) {
throw new IllegalArgumentException(
"Username specified is not valid for this transport type.");
}
final Collection<Registration> registrations = RegistrationManager
.getInstance().getRegistrations(jid, parent.transportType);
boolean foundReg = false;
boolean triggerRestart = false;
for (final Registration registration : registrations) {
if (!registration.getUsername().equals(username)) {
Log.debug("Deleting existing registration before"
+ " creating a new one: " + registration);
RegistrationManager.getInstance().deleteRegistration(
registration);
}
else {
Log.debug("Existing registration found that can be updated: "
+ registration);
if ((registration.getPassword() != null && password == null)
|| (registration.getPassword() == null && password != null)
|| (registration.getPassword() != null
&& password != null && !registration
.getPassword().equals(password))) {
Log.debug("Updating password for existing registration: "
+ registration);
registration.setPassword(password);
triggerRestart = true;
}
if ((registration.getNickname() != null && nickname == null)
|| (registration.getNickname() == null && nickname != null)
|| (registration.getNickname() != null
&& nickname != null && !registration
.getNickname().equals(nickname))) {
Log.debug("Updating nickname for existing registration: "
+ registration);
registration.setNickname(nickname);
triggerRestart = true;
}
foundReg = true;
}
// if a change was made to the registration, restart it.
if (triggerRestart) {
try {
Log.debug("An existing registration was "
+ "updated. Restarting the related session: "
+ registration);
final TransportSession relatedSession = parent.sessionManager
.getSession(registration.getJID().getNode());
parent.registrationLoggedOut(relatedSession);
}
catch (NotFoundException e) {
// No worries, move on.
}
}
}
if (!foundReg) {
RegistrationManager.getInstance().createRegistration(jid,
parent.transportType, username, password, nickname);
triggerRestart = true;
}
if (triggerRestart) {
Log.debug("Clean up any leftover roster items "
+ "from other transports for: " + jid);
try {
parent.cleanUpRoster(jid, !noRosterItem);
}
catch (UserNotFoundException ee) {
throw new UserNotFoundException("Unable to find roster.");
}
}
if (!noRosterItem) {
try {
Log.debug("Adding Transport roster item to the roster of: "
+ jid);
parent.addOrUpdateRosterItem(jid, parent.getJID(), parent
.getDescription(), "Transports");
}
catch (UserNotFoundException e) {
throw new UserNotFoundException(
"User not registered with server.");
}
}
else {
Log.debug("Not adding Transport roster item to the roster of: "
+ jid + " (as this was explicitly requested).");
}
}
/**
* Removes a registration from this transport.
*
* @param jid JID of user to add registration to.
* @throws UserNotFoundException if registration or roster not found.
*/
public void deleteRegistration(JID jid) throws UserNotFoundException {
Collection<Registration> registrations = RegistrationManager
.getInstance().getRegistrations(jid, parent.transportType);
if (registrations.isEmpty()) {
throw new UserNotFoundException("User was not registered.");
}
// Log out any active sessions.
try {
TransportSession session = parent.sessionManager.getSession(jid);
if (session.isLoggedIn()) {
parent.registrationLoggedOut(session);
}
parent.sessionManager.removeSession(jid);
}
catch (NotFoundException e) {
// Ok then.
}
// For now, we're going to have to just nuke all of these. Sorry.
for (final Registration reg : registrations) {
RegistrationManager.getInstance().deleteRegistration(reg);
}
// Clean up the user's contact list.
try {
parent.cleanUpRoster(jid, false, true);
}
catch (UserNotFoundException e) {
throw new UserNotFoundException("Unable to find roster.");
}
}
}