package com.heatonresearch.aol.toc2;
import java.net.*;
import java.io.*;
import java.util.*;
/**
* 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)
* @author John Connolly
*
* Revised February 4th 2008
*
*
* @version 2.0
*/
public class JavaTOC2 implements Runnable {
private Thread runner = null;
private static final int REST_TIME = 33;
/**
* 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:AIMMotionNotifier by John Connolly";
/**
* 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;
/**
* 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;
}
public void start() {
if (runner == null) {
runner = new Thread(this);
runner.start();
}
}
/**
* 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 {
this.id = id;
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
*/
protected 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 {
if (is.read() != '*')
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 run() {
System.out.println("Thread running...");
while (true) {
try {
String str = this.getFlap();
if (str == null) {
Thread.sleep(33);
continue;
}
if (str.toUpperCase().startsWith("IM_IN2:")) {
handleIM(str);
} else if (str.toUpperCase().startsWith("ERROR:")) {
handleError(str);
} else {
owner.unknown(str);
}
Thread.sleep(33);
} catch (Exception e) {
e.printStackTrace();
}
;
}
}
/**
* 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) {
System.out.println("Sending " + to + ": " + "msg");
try {
this.sendFlap(DATA, "toc_send_im " + normalize(to) + " \""
+ encode(msg) + "\"");
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
/**
* 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 '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 += " ";
break;
case '{':
case '}':
case '\\':
case '"':
rtn += "\\";
default:
rtn += str.charAt(i);
}
}
return rtn;
}
}