hudson.plugins.jabber.im.transport.JabberIMConnection Maven / Gradle / Ivy
The newest version!
/*
* Created on 06.03.2007
*/
package hudson.plugins.jabber.im.transport;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.net.ssl.SSLSocketFactory;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.Roster.SubscriptionMode;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.MessageTypeFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.ToContainsFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.RosterPacket.ItemType;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.packet.DelayInformation;
import org.jivesoftware.smackx.packet.MessageEvent;
import org.jivesoftware.smackx.packet.XHTMLExtension;
import hudson.plugins.im.AbstractIMConnection;
import hudson.plugins.im.AuthenticationHolder;
import hudson.plugins.im.GroupChatIMMessageTarget;
import hudson.plugins.im.IMConnection;
import hudson.plugins.im.IMConnectionListener;
import hudson.plugins.im.IMException;
import hudson.plugins.im.IMMessageTarget;
import hudson.plugins.im.IMPresence;
import hudson.plugins.im.bot.Bot;
import hudson.plugins.im.tools.Assert;
import hudson.plugins.im.tools.ExceptionHelper;
/**
* Smack-specific implementation of {@link IMConnection}.
*
* @author kutzi
* @author Uwe Schaefer (original author)
*/
class JabberIMConnection extends AbstractIMConnection {
private static final Logger LOGGER = Logger.getLogger(JabberIMConnection.class.getName());
private volatile XMPPConnection connection;
private final Map> groupChatCache = new HashMap>();
private final Map> chatCache = new HashMap>();
private final Set bots = new HashSet();
private final String passwd;
private final String botCommandPrefix;
/**
* Jabber 'nick'. This is just the username-part of the Jabber-ID.
* I.e. for '[email protected]' it is 'john.doe'.
*/
private final String nick;
/**
* The nick name of the Hudson bot to use in group chats.
* May be null in which case the nick is used.
*/
private final String groupChatNick;
/**
* Server name of the Jabber server.
*/
private final String hostname;
private final int port;
private final String[] groupChats;
private IMPresence impresence;
private String imStatusMessage;
private boolean enableSASL;
private final JabberPublisherDescriptor desc;
private final AuthenticationHolder authentication;
private Roster roster;
static {
SmackConfiguration.setPacketReplyTimeout(20000);
System.setProperty("smack.debuggerClass", JabberConnectionDebugger.class.getName());
}
JabberIMConnection(JabberPublisherDescriptor desc, AuthenticationHolder authentication) throws IMException {
super(desc);
Assert.isNotNull(desc, "Parameter 'desc' must not be null.");
this.desc = desc;
this.authentication = authentication;
this.hostname = desc.getHost();
this.port = desc.getPort();
this.nick = JabberUtil.getUserPart(desc.getJabberId());
this.passwd = desc.getPassword();
this.enableSASL = desc.isEnableSASL();
this.groupChatNick = desc.getGroupChatNickname() != null ?
desc.getGroupChatNickname() : this.nick;
this.botCommandPrefix = desc.getCommandPrefix();
if (desc.getInitialGroupChats() != null) {
this.groupChats = desc.getInitialGroupChats().trim().split("\\s");
} else {
this.groupChats = new String[0];
}
this.impresence = desc.isExposePresence() ? IMPresence.AVAILABLE : IMPresence.UNAVAILABLE;
}
@Override
public boolean connect() {
lock();
try {
try {
if (!isConnected()) {
if (createConnection()) {
LOGGER.info("Connected to XMPP on "
+ this.connection.getHost() + ":" + this.connection.getPort()
+ "/" + this.connection.getServiceName()
+ (this.connection.isUsingTLS() ? " using TLS" : "")
+ (this.connection.isUsingCompression() ? " using compression" : ""));
// I've read somewhere that status must be set, before one can do anything other
// Don't know if it's true, but can't hurt, either.
sendPresence();
groupChatCache.clear();
for (String groupChatName : this.groupChats) {
try {
groupChatName = groupChatName.trim();
getOrCreateGroupChat(groupChatName);
LOGGER.info("Joined groupchat " + groupChatName);
} catch (IMException e) {
// if we got here, the XMPP connection could be established, but probably the groupchat name
// is invalid
LOGGER.warning("Unable to connect to groupchat '" + groupChatName + "'. Did you append @conference or so to the name?\n"
+ "Exception: " + ExceptionHelper.dump(e));
}
}
} else {
// clean-up if needed
if (this.connection != null) {
try {
this.connection.disconnect();
} catch (Exception e) {
// ignore
}
}
return false;
}
}
return true;
} catch (final Exception e) {
LOGGER.warning(ExceptionHelper.dump(e));
return false;
}
} finally {
unlock();
}
}
@Override
public void close() {
lock();
try {
try {
for (WeakReference entry : groupChatCache.values()) {
MultiUserChat chat = entry.get();
if (chat != null && chat.isJoined()) {
chat.leave();
}
}
// there seems to be no way to leave a 1-on-1 chat with Smack
this.groupChatCache.clear();
this.chatCache.clear();
if (this.connection.isConnected()) {
this.connection.disconnect();
}
} catch (Exception e) {
// ignore
LOGGER.fine(e.toString());
} finally {
this.connection = null;
}
} finally {
unlock();
}
}
private boolean createConnection() throws XMPPException {
if (this.connection != null) {
try {
this.connection.disconnect();
} catch (Exception ignore) {
// ignore
}
}
String serviceName = desc.getServiceName();
final ConnectionConfiguration cfg;
if (serviceName == null) {
cfg = new ConnectionConfiguration(
this.hostname, this.port);
} else if (this.hostname == null) {
cfg = new ConnectionConfiguration(serviceName);
} else {
cfg = new ConnectionConfiguration(
this.hostname, this.port,
serviceName);
}
// Currently, we handle reconnects ourself.
// Maybe we should change it in the future, but currently I'm
// not sure what Smack's reconnect feature really does.
cfg.setReconnectionAllowed(false);
cfg.setDebuggerEnabled(true);
// try workaround for SASL error in Smack 3.1.0
// See: HUDSON-6032
// http://www.igniterealtime.org/community/message/198558
// and also http://www.igniterealtime.org/community/message/201908#201908
SASLAuthentication.unregisterSASLMechanism("DIGEST-MD5");
//SASLAuthentication.unregisterSASLMechanism("GSSAPI");
cfg.setSASLAuthenticationEnabled(this.enableSASL);
boolean retryWithLegacySSL = false;
Exception originalException = null;
try {
this.connection = new XMPPConnection(cfg);
this.connection.connect();
if (!this.connection.isConnected()) {
retryWithLegacySSL = true;
}
} catch (XMPPException e) {
retryWithLegacySSL = true;
originalException = e;
}
if (retryWithLegacySSL) {
retryConnectionWithLegacySSL(cfg, originalException);
}
if (this.connection.isConnected()) {
this.connection.login(this.desc.getUserName(), this.passwd, "Hudson");
setupSubscriptionMode();
listenForPrivateChats();
}
return this.connection.isAuthenticated();
}
/**
* Transparently retries the connection attempt with legacy SSL if original attempt fails.
* @param originalException the exception of the original attempt (may be null)
*
* See HUDSON-6863
*/
private void retryConnectionWithLegacySSL(
final ConnectionConfiguration cfg, Exception originalException)
throws XMPPException {
try {
LOGGER.info("Retrying connection with legacy SSL");
cfg.setSocketFactory(SSLSocketFactory.getDefault());
this.connection = new XMPPConnection(cfg);
this.connection.connect();
} catch (XMPPException e) {
if (originalException != null) {
// use the original connection exception as legacy SSL should only
// be a fallback
throw new XMPPException(originalException);
} else {
throw new XMPPException(e);
}
}
}
/**
* Sets the chosen subscription mode on our connection.
*/
private void setupSubscriptionMode() {
this.roster = this.connection.getRoster();
SubscriptionMode mode = SubscriptionMode.valueOf(this.desc.getSubscriptionMode());
switch (mode) {
case accept_all : LOGGER.info("Accepting all subscription requests");
break;
case reject_all : LOGGER.info("Rejecting all subscription requests");
break;
case manual : LOGGER.info("Subscription requests must be handled manually");
break;
}
this.roster.setSubscriptionMode(mode);
}
/**
* Listens on the connection for private chat requests.
*/
private void listenForPrivateChats() {
PacketFilter filter = new AndFilter(new MessageTypeFilter(Message.Type.chat),
new ToContainsFilter(this.desc.getUserName()));
// Actually, this should be the full user name (including '@server')
// but since via this connection only message to me should be delivered (right?)
// this doesn't matter anyway.
PacketListener listener = new PrivateChatListener();
this.connection.addPacketListener(listener, filter);
}
private MultiUserChat getOrCreateGroupChat(String groupChatName) throws IMException {
WeakReference ref = groupChatCache.get(groupChatName);
MultiUserChat groupChat = null;
if (ref != null) {
groupChat = ref.get();
}
boolean create = (groupChat == null);
if (create) {
groupChat = new MultiUserChat(this.connection, groupChatName);
try {
groupChat.join(this.groupChatNick);
} catch (XMPPException e) {
LOGGER.warning("Cannot join group chat '" + groupChatName + "'. Exception:\n" + ExceptionHelper.dump(e));
throw new IMException(e);
}
// get rid of old messages:
while (groupChat.pollMessage() != null) {
}
this.bots.add(new Bot(new JabberMultiUserChat(groupChat, this),
this.groupChatNick, this.desc.getHost(),
this.botCommandPrefix, this.authentication));
groupChatCache.put(groupChatName, new WeakReference(groupChat));
}
return groupChat;
}
private Chat getOrCreatePrivateChat(String chatPartner, Message msg) {
// use possibly existing chat
WeakReference wr = chatCache.get(chatPartner);
if (wr != null) {
Chat c = wr.get();
if (c != null) {
return c;
}
}
final Chat chat = this.connection.getChatManager().createChat(chatPartner, null);
Bot bot = new Bot(new JabberChat(chat, this), this.groupChatNick,
this.desc.getHost(), this.botCommandPrefix, this.authentication);
this.bots.add(bot);
if (msg != null) {
// replay original message:
bot.onMessage(new JabberMessage(msg, isAuthorized(msg.getFrom())));
}
chatCache.put(chatPartner, new WeakReference(chat));
return chat;
}
public void send(final IMMessageTarget target, final String text)
throws IMException {
Assert.isNotNull(target, "Parameter 'target' must not be null.");
Assert.isNotNull(text, "Parameter 'text' must not be null.");
try {
// prevent long waits for lock
if (!tryLock(5, TimeUnit.SECONDS)) {
return;
}
try {
if (target instanceof GroupChatIMMessageTarget) {
getOrCreateGroupChat(target.toString()).sendMessage(
text);
} else {
final Chat chat = getOrCreatePrivateChat(target.toString(), null);
chat.sendMessage(text);
}
} catch (final XMPPException e) {
// server unavailable ? Target-host unknown ? Well. Just skip this
// one.
LOGGER.warning(ExceptionHelper.dump(e));
// TODO ? tryReconnect();
} finally {
unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// ignore
}
}
/**
* This implementation ignores the new presence if
* {@link JabberPublisherDescriptor#isExposePresence()} is false.
*/
@Override
public void setPresence(final IMPresence impresence, String statusMessage)
throws IMException {
Assert.isNotNull(impresence, "Parameter 'impresence' must not be null.");
if (this.desc.isExposePresence()) {
this.impresence = impresence;
this.imStatusMessage = statusMessage;
sendPresence();
} else {
// Ignore new presence.
// Don't re-send presence, either. It would result in disconnecting from
// all joined group chats
}
}
private void sendPresence() {
try {
// prevent long waits for lock
if (!tryLock(5, TimeUnit.SECONDS)) {
return;
}
try {
if( !isConnected() ) {
return;
}
Presence presence;
switch (this.impresence) {
case AVAILABLE:
presence = new Presence(Presence.Type.available,
this.imStatusMessage, 1, Presence.Mode.available);
break;
case OCCUPIED:
presence = new Presence(Presence.Type.available,
this.imStatusMessage, 1, Presence.Mode.away);
break;
case DND:
presence = new Presence(Presence.Type.available,
this.imStatusMessage, 1, Presence.Mode.dnd);
break;
case UNAVAILABLE:
presence = new Presence(Presence.Type.unavailable);
break;
default:
throw new IllegalStateException("Don't know how to handle "
+ impresence);
}
this.connection.sendPacket(presence);
} finally {
unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// ignore
}
}
@Override
public boolean isConnected() {
lock();
try {
return this.connection != null && this.connection.isAuthenticated();
} finally {
unlock();
}
}
public boolean isAuthorized(String xmppAddress) {
String bareAddress = StringUtils.parseBareAddress(xmppAddress);
RosterEntry entry = this.roster.getEntry(bareAddress);
boolean authorized = entry != null
&& (entry.getType() == ItemType.both
|| entry.getType() == ItemType.from);
return authorized;
}
private final Map listeners =
new ConcurrentHashMap();
@Override
public void addConnectionListener(final IMConnectionListener listener) {
lock();
try {
ConnectionListener l = new ConnectionListener() {
@Override
public void connectionClosedOnError(Exception e) {
listener.connectionBroken(e);
}
@Override
public void connectionClosed() {
}
@Override
public void reconnectingIn(int paramInt) {
}
@Override
public void reconnectionFailed(Exception paramException) {
}
@Override
public void reconnectionSuccessful() {
}
};
listeners.put(listener, l);
this.connection.addConnectionListener(l);
} finally {
unlock();
}
}
@Override
public void removeConnectionListener(IMConnectionListener listener) {
lock();
try {
ConnectionListener l = this.listeners.remove(listener);
if (l != null) {
this.connection.removeConnectionListener(l);
} else {
LOGGER.warning("Connection listener " + listener + " not found.");
}
} finally {
unlock();
}
}
/**
* Listens for private chats.
*/
private final class PrivateChatListener implements PacketListener {
public void processPacket(Packet packet) {
if (packet instanceof Message) {
Message m = (Message)packet;
boolean composing = false;
boolean xhtmlMessage = false;
for (PacketExtension ext : m.getExtensions()) {
if (ext instanceof DelayInformation) {
// ignore delayed messages
return;
}
if (ext instanceof MessageEvent) {
MessageEvent me = (MessageEvent)ext;
if (me.isComposing()) {
// ignore messages which are still being composed
composing = true;
}
}
if (ext instanceof XHTMLExtension) {
xhtmlMessage = true;
}
}
if (composing && !xhtmlMessage) {
// pretty strange: if composing extension AND NOT XHTMLExtension, this seems
// to mean that the message was delivered
return;
}
if (m.getBody() != null) {
LOGGER.info("Message from " + m.getFrom() + " : " + m.getBody());
final String chatPartner = m.getFrom();
getOrCreatePrivateChat(chatPartner, m);
}
}
}
};
}