package net.reliableresponse.notification.aim; import java.net.*; import java.io.*; import java.util.*; import net.reliableresponse.notification.broker.BrokerFactory; /** * The JavaTOC framework is a set of classes used to allow * a Java program to communicate with AOL's TOC2 protocol. * The Chatable interface and JavaTOC2 classes can easily * be moved to any program needing TOC abilities. * * Copyright 2005 by Jeff Heaton * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * @author Jeff Heaton(http://www.heatonresearch.com) * @version 2.0 */ public class JavaTOC2 { /** * The host address of the TOC server */ public String tocHost = "toc.oscar.aol.com"; /** * The port used to connect to the TOC server */ public int tocPort = 9898; /** * The OSCAR authentication server */ public String authHost = "login.oscar.aol.com"; /** * The OSCAR authentication server's port */ public int authPort = 5190; /** * What language to use */ public String language = "english"; /** * The version of the client */ public String version = "TIC:JavaTOC by Jeff Heaton"; /** * The string used to "roast" passwords. See * the roastPassword method for more info. */ public String roastString = "Tic/Toc"; /** * The sequence number used for FLAP packets. */ protected short sequence; /** * The connection to the TOC server. */ protected Socket connection; /** * An InputStream to the connection */ protected InputStream is; /** * An OutputStream to the connection */ protected OutputStream os; /** * Screen name of current user */ protected String id; /** * The ChatBuddies object that owns this object. */ protected Chatable owner; /** * The ID number for a SIGNON packet(FLAP) */ public static final int SIGNON = 1; /** * The ID number for a DATA packet (flap) */ public static final int DATA = 2; /** * This stores the password for reconnecting */ String password; /** * The constructor. Specifies the Chatable interface class * to report to. * * @param owner The object to report events to */ public JavaTOC2(Chatable owner) { this.owner = owner; } /** * Log in to TOC * * @param id The screen name to login with * @param password The screen name's password * @return true on success * @exception java.io.IOException */ public boolean login(String id,String password) throws IOException { BrokerFactory.getLoggingBroker().logDebug("Logging into AIM as "+id); this.id = id; this.password = password; connection = new Socket(tocHost,tocPort); is = connection.getInputStream(); os = connection.getOutputStream(); sendRaw("FLAPON\r\n\r\n"); getFlap(); sendFlapSignon(); String command = "toc2_signon " + authHost + " " + authPort + " " + id + " " + roastPassword(password) + " " + language + " \"" + this.version + "\" 160 " + calculateCode(id,password); sendFlap(DATA,command); String str = getFlap(); if ( str.toUpperCase().startsWith("ERROR:") ) { handleError(str); return false; } this.sendFlap(DATA,"toc_add_buddy " + this.id); this.sendFlap(DATA,"toc_init_done"); this.sendFlap(DATA,"toc_set_caps 09461343-4C7F-11D1-8222-444553540000 09461348-4C7F-11D1-8222-444553540000"); this.sendFlap(DATA,"toc_add_permit "); this.sendFlap(DATA,"toc_add_deny "); return true; } /** * Logout of toc and close the socket */ public void logout() { try { connection.close(); is.close(); os.close(); } catch ( IOException e ) { } } /** * Called to roast the password. * * Passwords are roasted when sent to the host. This is done so they * aren't sent in "clear text" over the wire, although they are still * trivial to decode. Roasting is performed by first xoring each byte * in the password with the equivalent modulo byte in the roasting * string. The result is then converted to ascii hex, and prepended * with "0x". So for example the password "password" roasts to * "0x2408105c23001130" * * @param str The password to roast * @return The password roasted */ protected String roastPassword(String str) { byte xor[] = roastString.getBytes(); int xorIndex = 0; String rtn = "0x"; for ( int i=0;i<str.length();i++ ) { String hex = Integer.toHexString(xor[xorIndex]^(int)str.charAt(i)); if ( hex.length()==1 ) hex = "0"+hex; rtn+=hex; xorIndex++; if ( xorIndex==xor.length ) xorIndex=0; } return rtn; } /** * Calculate a login security code from the user id and * password. * * @param uid The user id to encode * @param pwd The password to encoude * @return The code, which is used to login */ protected int calculateCode(String uid,String pwd) { int sn = uid.charAt(0)-96; int pw = pwd.charAt(0)-96; int a = sn * 7696 + 738816; int b = sn * 746512; int c = pw * a; return( c - a + b + 71665152 ); } /** * Send a string over the socket as raw bytes * * @param str The string to send * @exception java.io.IOException */ protected void sendRaw(String str) throws IOException { os.write(str.getBytes()); } /** * Write a little endian word * * @param word A word to write * @exception java.io.IOException */ protected void writeWord(short word) throws IOException { os.write((byte) ((word >> 8) & 0xff) ); os.write( (byte) (word & 0xff) ); } /** * Send a FLAP signon packet * * @exception java.io.IOException */ protected void sendFlapSignon() throws IOException { int length = 8+id.length(); sequence++; os.write((byte)'*'); os.write((byte)SIGNON); writeWord(sequence); writeWord((short)length); os.write(0); os.write(0); os.write(0); os.write(1); os.write(0); os.write(1); writeWord((short)id.length()); os.write(id.getBytes()); os.flush(); } /** * Send a FLAP packet * * @param type The type DATA or SIGNON * @param str The string message to send * @exception java.io.IOException */ public void sendFlap(int type,String str) throws IOException { int length = str.length()+1; sequence++; os.write((byte)'*'); os.write((byte)type); writeWord(sequence); writeWord((short)length); os.write(str.getBytes()); os.write(0); os.flush(); } /** * Get a FLAP packet * * @return The data as a string * @exception java.io.IOException */ protected String getFlap() throws IOException { int firstByte = -1; try { firstByte = is.read(); } catch (IOException e1) { BrokerFactory.getLoggingBroker().logWarn(e1); } if (firstByte == -1) { // The connection is closed. 1st sleep, then try to reconnect try { Thread.sleep(10000); } catch (InterruptedException e) { BrokerFactory.getLoggingBroker().logWarn(e); } login (id, password); } else if ( firstByte !='*' ) { return null; } is.read(); is.read(); is.read(); int length = (is.read()*0x100)+is.read(); byte b[] = new byte[length]; is.read(b); return new String(b); } /** * Begin processing all TOC events and sending them to the * Chatable class. Usually called from a thread. * * @exception java.io.IOException */ public void processTOCEvents() throws IOException { for ( ;; ) { String str = this.getFlap(); if ( str==null ) continue; if ( str.toUpperCase().startsWith("IM_IN2:") ) { handleIM(str); } else if ( str.toUpperCase().startsWith("ERROR:") ) { handleError(str); } else { owner.unknown(str); } } } /** * Extract the next element, bounded by a ':', and * return that element. This has the advantage over StringTokenizer, * in that at any point you can pull the remaining string, just * by using what is left of the StringBuffer. This is good * for when there is a fixed number of tokens, yet the remaining * String can still use the token. For example: * * JeffHeatonDotCom:F:F:Please remember to get: this, that and that. * * The final : is not a token because this string only has 4 fields. * * @param str The string to parse, will be modified * @return The next element found */ protected String nextElement(StringBuffer str) { String result=""; int i = str.indexOf(":",0); if(i==-1) { result = str.toString(); str.setLength(0); } else { result = str.substring(0,i).toString(); String a = str.substring(i+1); str.setLength(0); str.append(a); } return result; } /** * Parse an error packet and send it back to the Chatable class * * @param str The error */ protected void handleError(String str) { StringBuffer sb = new StringBuffer(str); String e = nextElement(sb); String v = nextElement(sb); owner.error( e,v); } /** * Parse an instant message and send it back to the Chatable * class * * @param str The instant message */ protected void handleIM(String str) { StringBuffer sb = new StringBuffer(str); nextElement(sb); // get from String from = nextElement(sb); // get a String a = nextElement(sb); // get b String b = nextElement(sb); // get message String message = sb.toString(); owner.im(from,message); } /** * Send a IM * * @param to Screen name to send an IM to * @param msg The instant message */ public void send(String to,String msg) { try { this.sendFlap(DATA,"toc_send_im " + normalize(to) + " \"" + encode(msg) + "\""); } catch ( java.io.IOException e ) { } } /** * Called to normalize a screen name. This removes all spaces * and converts the name to lower case. * * @param name The screen name * @return The normalized screen name */ protected String normalize(String name) { String rtn=""; for ( int i=0;i<name.length();i++ ) { if ( name.charAt(i)==' ' ) continue; rtn+=Character.toLowerCase(name.charAt(i)); } return rtn; } /** * Called to encode a message. Convert carige returns to <br>'s * and put \'s infront of quotes, etc. * * @param str The string to be encoded * @return The string encoded */ protected String encode(String str) { String rtn=""; for ( int i=0;i<str.length();i++ ) { switch ( str.charAt(i) ) { case '\r': rtn+="<br>"; break; case '{': case '}': case '\\': case '"': rtn+="\\"; default: rtn+=str.charAt(i); } } return rtn; } }