/*
*
* Hamsam - Instant Messaging API
*
* Copyright (C) 2003 Mike Miller <mikemil@users.sourceforge.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package hamsam.protocol.aim.command;
import java.io.IOException;
import java.util.*;
import java.util.Enumeration;
import java.util.Map;
import java.util.Vector;
import java.util.logging.Logger;
import hamsam.api.*;
import hamsam.api.Buddy;
import hamsam.api.Message;
import hamsam.api.MessageComponent;
import hamsam.api.TextComponent;
import hamsam.exception.IllegalStateException;
import hamsam.net.Connection;
import hamsam.net.DirectConnection;
import hamsam.net.HttpConnection;
import hamsam.net.ProxyInfo;
import hamsam.net.SocksConnection;
import hamsam.util.log.LogManager;
import hamsam.protocol.Protocol;
import hamsam.protocol.aim.AIMReaderThread;
import hamsam.protocol.aim.AIMWriterThread;
import hamsam.protocol.aim.flap.FlapConstants;
import hamsam.protocol.aim.snac.SNACConstants;
import hamsam.protocol.aim.util.ByteUtils;
import hamsam.protocol.aim.util.TLV;
import hamsam.protocol.aim.util.TLVConstants;
/**
* @author mikem
*
*/
public class AuthCommandHandler implements CommandHandler, ConnectionHandler, Runnable {
//~ Static fields/initializers -----------------------------------------------------------------
Logger log = LogManager.getLogger();
/* command handler states */
private static final int DISCONNECTED = 0;
private static final int CONNECTING = 1; // sending client ident
private static final int DISCONNECTING = 3; // disconnecting from authentication server
private static final int BOS_CONNECTING = 4; // next we connect to the BOS server wtih cookie
private static final int BOS_CONNECTED = 5;
private static final int BOS_SERVICE_LIST = 6;
private static final int PROTOCOL_NEGOTIATION = 7;
private static final int ACK_CONNECT_RATES = 8;
private static final int CLIENT_READY = 9;
//~ Instance fields ----------------------------------------------------------------------------
/** This thread checks for incoming commands from AIM */
private AIMReaderThread reader;
/** This thread handles all outgoing commands to AIM. */
private AIMWriterThread writer;
/** This is a buffer for holding all incoming commands. Access to this buffer must be synchronized. */
private Vector readBuffer;
/** This is a buffer for holding all outgoing commands. Access to this buffer must be synchronized. */
private Vector writeBuffer;
/** internal state */
private int state;
/** connection type */
private ProxyInfo proxyInfo;
/** users screen name*/
private String screenName;
/** user's password */
private String password;
/** connection object */
private Connection conn;
/** AIM protocol instance */
private Protocol protocol;
private IMListener listener;
private boolean quit = false;
private int seqNum = 11;
//~ Constructors -------------------------------------------------------------------------------
/**
* Default Constructor
* @param protocol active protocol object
*/
public AuthCommandHandler(Protocol protocol) {
this.protocol = protocol;
state = DISCONNECTED;
readBuffer = new Vector();
writeBuffer = new Vector();
Thread thread = new Thread(this);
thread.setDaemon(true);
thread.setName("AuthCommandHandler");
thread.start();
}
//~ Methods ------------------------------------------------------------------------------------
/**
* Sends the initial login request to AIM
* @param info ProxyInfo object containing connection related information
* @param host host name of the server to connect to
* @param port port number to connect to
* @param user user's screen name
* @param password users password
*/
public void connect(ProxyInfo info, String host, int port, String user, String password)
throws IllegalStateException {
proxyInfo = info;
screenName = user;
this.password = password;
if (listener != null) {
listener.connecting(protocol);
}
// create the conection
if (connectToHost(host, port, CONNECTING)) {
startThreads();
sendToWriterThread(new LoginCmd(screenName, password));
} else {
log.severe("error connecting to " + host + " on port " + port);
}
}
/*
* Start the reader & writer threads
*/
private void startThreads() {
if (reader == null) {
reader = new AIMReaderThread(conn, readBuffer);
reader.start();
}
if (writer == null) {
writer = new AIMWriterThread(conn, writeBuffer);
writer.start();
}
}
/**
* Reconnect to host - used during the OSCAR authentication processing
* @param hostName
* @param port
* @throws IllegalStateException
*/
public void reconnect(String hostName, int port) throws IllegalStateException {
log.info("disconnecting from auth server/connecting to BOS server...");
// disconnect from authentication server / connect to BOS
disconnect();
if (connectToHost(hostName, port, BOS_CONNECTING)) {
startThreads();
}
}
/**
* Cleans up the connection info in the handler
*/
public void disconnect() {
screenName = null;
password = null;
state = DISCONNECTING;
writer.stopWriting();
reader.stopReading();
try {
conn.close();
} catch (IOException e) {
e.printStackTrace();
}
conn = null;
writer = null;
reader = null;
state = DISCONNECTED;
}
/** Request protocol shutdown
*
*/
public void shutdown() {
quit = true;
if (listener != null) {
listener.disconnected(protocol);
}
}
/**
* Handles the authentication related
* @see hamsam.protocol.aim.command.CommandHandler#handleCommand(hamsam.protocol.aim.command.CommandEvent)
*/
public void handleCommand(CommandEvent evt) {
log.info("incoming event <--- " + evt.toString());
Command cmd = evt.getCommand();
switch (state) {
case CONNECTING :
if (cmd.getChannel() == FlapConstants.FLAP_CHANNEL_DISCONNECT) {
// get the server and cookie from the command tlv
TLV tlvServer = cmd.getTLV(TLVConstants.TLV_TYPE_SERVER);
TLV tlvCookie = cmd.getTLV(TLVConstants.TLV_TYPE_COOKIE);
TLV tlvError = cmd.getTLV(TLVConstants.TLV_TYPE_ERROR_CODE);
if ((tlvServer != null) && (tlvCookie != null)) {
// hack the port to 22 now that work firewall blocks real port
int port = 5190;// 22;
try {
String server = null;
String serverCookie = tlvServer.getStringValue();
int pos = serverCookie.indexOf(':');
if (pos != -1) {
server = serverCookie.substring(0, pos);
port = Integer.parseInt(serverCookie.substring(pos+1));
} else {
server = serverCookie;
}
reconnect(server, port);
} catch (IllegalStateException e) {
e.printStackTrace();
}
// Login to BOS Server with Cookie
state = BOS_CONNECTING;
sendToWriterThread(new BOSLoginCmd(tlvCookie.getValue()));
} else if (tlvError != null) {
log.severe("Authentication error - error code=0x" + ByteUtils.toHexString(tlvError.getValue()));
}
}
break;
case BOS_CONNECTING :
if (cmd.getChannel() == FlapConstants.FLAP_CHANNEL_CONNECT) {
state = BOS_CONNECTED;
if (listener != null) {
listener.connected(protocol);
}
}
break;
case BOS_CONNECTED :
if (cmd.getChannel() == FlapConstants.FLAP_CHANNEL_SNAC) {
if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_GENERIC_SERVICE_CONTROLS
&& cmd.getSubType() == SNACConstants.FAMILY_LIST) {
state = BOS_SERVICE_LIST;
sendToWriterThread(new ClientFamilyVersionCmd());
}
}
break;
case BOS_SERVICE_LIST :
if (cmd.getChannel() == FlapConstants.FLAP_CHANNEL_SNAC) {
if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_GENERIC_SERVICE_CONTROLS) {
if (cmd.getSubType() == SNACConstants.MSG_OF_THE_DAY) {
log.info("ignoring the MSG OF THE DAY");
break;
} else if (cmd.getSubType() == SNACConstants.SRV_SVC_VERSIONS) {
state = PROTOCOL_NEGOTIATION;
sendToWriterThread(new RequestServerRateCmd());
}
break;
}
}
break;
case PROTOCOL_NEGOTIATION :
if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_GENERIC_SERVICE_CONTROLS) {
if (cmd.getSubType() == SNACConstants.SERVER_RATE_INFO) {
state = ACK_CONNECT_RATES;
sendToWriterThread(new AckServerRateInfo());
// send location info including capabilities
sendToWriterThread(new ClientSetLocationInfoCmd());
// send the 'activate SSI' which starts the buddy notifications (on/off line) flowing
// sendToWriterThread(
// new BaseCmd(
// 7,
// SNACConstants.SNAC_FAMILY_SERVER_STORED_INFO,
// SNACConstants.ACTIVATE_SERVER_SSI));
sendToWriterThread(
new BaseCmd(
7,
SNACConstants.SNAC_FAMILY_SERVER_STORED_INFO,
SNACConstants.REQUEST_CONTACT_LIST));
// now short-cut directly to Client-Ready cmd
// sendToWriterThread(new ClientReadyCmd());
// state = CLIENT_READY;
// sendToWriterThread(new RequestICBMParmCmd(seqNum++));
}
}
break;
case ACK_CONNECT_RATES :
if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_SERVER_STORED_INFO) {
switch (cmd.getSubType()) {
case SNACConstants.CONTACT_LIST_RESPONSE :
ContactListResponseCmd contactListResponse = new ContactListResponseCmd(cmd);
buddyListReceived(contactListResponse.getGroups());
// send the 'activate SSI' which starts the buddy notifications (on/off line) flowing
sendToWriterThread(
new BaseCmd(
8,
SNACConstants.SNAC_FAMILY_SERVER_STORED_INFO,
SNACConstants.ACTIVATE_SERVER_SSI));
sendToWriterThread(new ClientSetStatusCmd(9, 0));
//now short-cut directly to Client-Ready cmd
sendToWriterThread(new ClientReadyCmd());
state = CLIENT_READY;
sendToWriterThread(new RequestICBMParmCmd(seqNum++));
break;
}
}
break;
case CLIENT_READY :
if (cmd.getSubType() == SNACConstants.SNAC_SUBTYPE_ERROR) {
log.severe("error code=" + ByteUtils.toHexString(cmd.getSNACData()));
} else if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_MESSAGING) {
switch (cmd.getSubType()) {
case SNACConstants.SRV_ICBM_ERROR :
//System.out.println("ICMB error : ");
//ByteUtils.dump(cmd.getSNACData());
break;
case SNACConstants.CLIENT_RECV_ICBM :
ServerICBM serverICBM = new ServerICBM(cmd);
log.info("ICMB text=" + serverICBM.getMessageText());
break;
case SNACConstants.RESPONSE_ICBM_PARMINFO :
byte[] snacData = cmd.getSNACData();
log.fine("ICBM parm info response" + ByteUtils.toHexString(snacData));
// bit4 should be set to 1 for mini typing notifications
long flags = ByteUtils.getUInt(snacData, 2);
flags |= 8;
sendToWriterThread(new ClientSetICBMParmsCmd(seqNum++, (int) flags, snacData));
break;
// test to see if this is how I receive typing notifications!
// none received as of yet!!!
case SNACConstants.SNAC_SUBTYPE_MINI_TYPING_NOTIFICATION :
//System.out.println("Typing notification received!");
log.fine("Typing notification received");
//todo: figure out if typing started or stopped and
// call the appropriate listener method
break;
}
} else if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_BUDDY_LIST_MANAGEMENT) {
switch (cmd.getSubType()) {
case SNACConstants.SNAC_SUBTYPE_USER_ONLINE :
case SNACConstants.SNAC_SUBTYPE_USER_OFFLINE :
byte[] data = cmd.getSNACData();
int nameLen = data[0];
String name = ByteUtils.toString(data, 1, nameLen);
String status =
cmd.getSubType() == SNACConstants.SNAC_SUBTYPE_USER_ONLINE ? " online" : " offline";
log.info("User " + name + status);
break;
}
} else if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_SERVER_STORED_INFO) {
switch (cmd.getSubType()) {
case SNACConstants.CONTACT_LIST_RESPONSE :
ContactListResponseCmd contactListResponse = new ContactListResponseCmd(cmd);
buddyListReceived(contactListResponse.getGroups());
break;
}
} else if (cmd.getFamily() == SNACConstants.SNAC_FAMILY_GENERIC_SERVICE_CONTROLS) {
switch (cmd.getSubType()) {
case SNACConstants.SRV_ONLINE_INFO :
System.out.println("Server Online Info: ");
ByteUtils.dump(cmd.getSNACData());
break;
}
} // todo - include handling for snac(04/0c) - ICBM send ACK
break;
}
}
/*
* Connects to a server
*/
private boolean connectToHost(String host, int port, int newState) throws IllegalStateException {
log.info("creating connection to host " + host + " port " + port);
state = newState;
// Create a connection object
try {
switch (proxyInfo.getProxyType()) {
case ProxyInfo.DIRECT :
this.conn = new DirectConnection(host, port);
break;
case ProxyInfo.SOCKS4 :
String proxyHost = proxyInfo.getServerName();
int proxyPort = proxyInfo.getServerPort();
this.conn = new SocksConnection(proxyHost, proxyPort, host, port);
break;
case ProxyInfo.SOCKS5 :
proxyHost = proxyInfo.getServerName();
proxyPort = proxyInfo.getServerPort();
String proxyUsername = proxyInfo.getUsername();
String proxyPassword = proxyInfo.getPassword();
this.conn = new SocksConnection(proxyHost, proxyPort, proxyUsername, proxyPassword, host, port);
break;
case ProxyInfo.HTTP :
proxyHost = proxyInfo.getServerName();
proxyPort = proxyInfo.getServerPort();
proxyUsername = proxyInfo.getUsername();
proxyPassword = proxyInfo.getPassword();
if ((proxyUsername == null) || (proxyPassword == null)) {
this.conn = new HttpConnection(proxyHost, proxyPort, host, port);
} else {
this.conn = new HttpConnection(proxyHost, proxyPort, proxyUsername, proxyPassword, host, port);
}
break;
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/** Send a buddy an instant message
* @param buddy Buddy
* @param message Message
* @see hamsam.protocol.aim.command.CommandHandler#sendInstantMessage(hamsam.api.Buddy, hamsam.api.Message)
*/
public void sendInstantMessage(Buddy buddy, Message message) {
StringBuffer sb = new StringBuffer();
Enumeration e = message.getComponents();
while (e.hasMoreElements()) {
MessageComponent comp = (MessageComponent) e.nextElement();
if (comp instanceof TextComponent) {
sb.append(((TextComponent) comp).getSequence());
}
//else if(comp instanceof SmileyComponent)
//{
// // handle smiley component
//}
//else if(comp instanceof URLComponent)
//{
// // handle URL component
//}
}
sendToWriterThread(new ClientICMB(seqNum++, buddy.getUsername(), sb.toString()));
}
/*
* Send a packet to the writer thread for despatch to server.
*/
private void sendToWriterThread(Command cmd) {
synchronized (writeBuffer) {
writeBuffer.add(cmd);
writeBuffer.notify();
}
}
/**
* Processes the incoming events from the reader thread
* @see java.lang.Runnable#run()
*/
public void run() {
CommandEvent evt = null;
while (!quit) {
synchronized (readBuffer) {
while (readBuffer.isEmpty()) {
try {
readBuffer.wait();
} catch (InterruptedException e) {
}
}
if (readBuffer.isEmpty()) {
continue;
}
evt = (CommandEvent) readBuffer.remove(0);
}
handleCommand(evt);
evt = null;
}
}
/*
* Build list of Buddy objects to past to the listener. We call this
* as the last part of login.
*/
private void buddyListReceived(Map map) {
List buddyList = new LinkedList();
if (map != null) {
for (Iterator iter = map.keySet().iterator(); iter.hasNext();) {
String groupName = (String) iter.next();
List list = (List) map.get(groupName);
for (Iterator iterator = list.iterator(); iterator.hasNext();) {
String buddy = (String) iterator.next();
buddyList.add(new Buddy(protocol, buddy, groupName));
}
}
}
Buddy[] buddies = new Buddy[buddyList.size()];
buddies = (Buddy[]) buddyList.toArray(buddies);
if (listener != null) {
listener.buddyListReceived(protocol, buddies);
}
}
/** Add a buddy to the Buddy List
* @param buddy Buddy to add to Buddy List
* @see hamsam.protocol.aim.command.CommandHandler#addToBuddyList(hamsam.api.Buddy)
*/
public void addToBuddyList(Buddy buddy) {
sendToWriterThread(new AddBuddyCmd(seqNum++, buddy));
}
/** Delete a buddy from the Buddy List
* @param buddy to delete from Buddy List
* @see hamsam.protocol.aim.command.CommandHandler#deleteFromBuddyList(hamsam.api.Buddy)
*/
public void deleteFromBuddyList(Buddy buddy) {
sendToWriterThread(new DeleteBuddyCmd(seqNum++, buddy));
}
/** Ignore a buddy
* @param buddy Buddy to ignore
* @see hamsam.protocol.aim.command.CommandHandler#ignoreBuddy(hamsam.api.Buddy)
*/
public void ignoreBuddy(Buddy buddy) {
sendToWriterThread(new IgnoreBuddyCmd(seqNum++, buddy));
}
/** Unignore a buddy
* @param buddy Buddy to unignore
* @see hamsam.protocol.aim.command.CommandHandler#unIgnoreBuddy(hamsam.api.Buddy)
*/
public void unIgnoreBuddy(Buddy buddy) {
sendToWriterThread(new UnignoreBuddyCmd(seqNum++, buddy));
}
/** Send typing started notification to a Buddy
* @param buddy Buddy that you are typing a message to.
* @see hamsam.protocol.aim.command.CommandHandler#typingStarted(hamsam.api.Buddy)
*/
public void typingStarted(Buddy buddy) {
sendToWriterThread(new TypingNotificationCmd(seqNum++, buddy, SNACConstants.MTN_TYPING_STARTED));
}
/** Send typing stopped notification to a Buddy
* @param buddy Buddy that you stopped typing a message to.
* @see hamsam.protocol.aim.command.CommandHandler#typingStopped(hamsam.api.Buddy)
*/
public void typingStopped(Buddy buddy) {
sendToWriterThread(new TypingNotificationCmd(seqNum++, buddy, SNACConstants.MTN_TYPING_FINISHED));
}
/* (non-Javadoc)
* @see hamsam.protocol.aim.command.CommandHandler#setListener(hamsam.api.IMListener)
*/
public void setListener(IMListener listener) {
this.listener = listener;
}
/* (non-Javadoc)
* @see hamsam.protocol.aim.command.CommandHandler#changeStatus(java.lang.Integer)
*/
public void changeStatus(Integer statusFlag) {
// this is how ICQ would do it
//sendToWriterThread( new ClientSetStatusCmd(seqNum++, statusFlag.intValue()));
sendToWriterThread( new ClientSetLocationInfoCmd(seqNum++, statusFlag.intValue()));
}
}