/*
* XNap
*
* A pure java file sharing client.
*
* See AUTHORS for copyright information.
*
* 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
*
*/
package xnap.plugin.nap.net;
import xnap.XNap;
import xnap.net.*;
import xnap.plugin.nap.Plugin;
import xnap.plugin.nap.net.msg.MessageHandler;
import xnap.plugin.nap.net.msg.SendQueue;
import xnap.plugin.nap.net.msg.MessageStrings;
import xnap.plugin.nap.net.msg.client.ClientMessage;
import xnap.plugin.nap.net.msg.client.ListChannelsMessage;
import xnap.plugin.nap.net.msg.client.NickCheckMessage;
import xnap.plugin.nap.net.msg.server.*;
import xnap.plugin.nap.util.Channel;
import xnap.plugin.nap.util.NapPreferences;
import xnap.util.*;
import xnap.util.prefs.StringValidator;
import xnap.io.*;
import java.beans.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.text.MessageFormat;
import org.apache.log4j.Logger;
public class Server extends AbstractRunnable implements IChatServer, Runnable {
//--- Constant(s) ---
// wait 30 seconds for login ack
//public static final int LOGIN_TIMEOUT = 30 * 1000;
public static final int SOCKET_TIMEOUT = 1 * 1000;
public static final int MAX_RETRY_ON_CONNECTION_REFUSED = 3;
public static final int STATUS_NOT_CONNECTED = 0;
public static final int STATUS_CONNECTING = 1;
public static final int STATUS_CONNECTED = 2;
public static final int STATUS_LOGIN_FAILED = 4;
// recoverable error
public static final int STATUS_FAILED = 5;
// unrecoverable error
public static final int STATUS_ERROR = 6;
// FIX ME
public static final int STATUS_NEW_STATS = 7;
public static final String[] STATUS_MSGS = {
"", Plugin.tr("connecting") + "...", Plugin.tr("connected"),
Plugin.tr("login failed"), Plugin.tr("failed"),
Plugin.tr("connected"), Plugin.tr("error"),
};
//--- Data field(s) ---
protected static Logger logger = Logger.getLogger(Server.class);
protected static Preferences prefs = Preferences.getInstance();
private String remoteHost;
private int remotePort;
private String remoteIP = null;
private String network;
private int userCount = -1;
private String username = null;
private String password = null;
private String email = null;
private boolean newUser;
private int fileCount = -1;
private int fileSize = -1;
private int ping = -1;
/**
* Server was fetched from index service.
*/
private boolean temporay = false;
private boolean redirector = false;
private String redirectedHost;
private int redirectedPort;
private Socket socket;
private InputStream in;
private OutputStream out;
private long lastLogin = 0;
private NapListener listener = null;
protected Search currentSearch = null;
protected Browse currentBrowse = null;
/**
* Remembers the last <code>ErrorMessage</code>.
*/
protected String lastError;
/**
* Queue of pending searches.
*/
private PriorityQueue searchQueue = new PriorityQueue();
/**
* Queue of pending browses.
*/
private LinkedList browseQueue = new LinkedList();
/**
* Queue of pending messages.
*/
//private SendQueue sendQueue = new SendQueue(this);
/* contains last n searchesTexts and their collectors */
private SearchResultCache searchCache = new SearchResultCache();
private long lastSearch = 0;
/**
* Hashes nick to user objects.
*/
protected Hashtable users = new Hashtable();
protected int searchCount = 0;
/**
* Hashes channel names to <code>Channel</code> objects.
*/
protected Hashtable channels = new Hashtable();
protected ServerVersion version = ServerVersion.UNKNOWN;
protected Range shared;
// do not flood server
private int maxPacketsPerTick
= NapPreferences.getInstance().getMaxPacketsPerTick();
private long tickLength
= NapPreferences.getInstance().getTickLength();
private long writeTickPacketCount = 0;
private long writeTickStart = 0;
//--- Constructor(s) ---
public Server(String host, String ip, int port, String network,
int fileCount, int fileSize, int userCount)
{
status = STATUS_NOT_CONNECTED;
this.remoteHost = host;
this.remoteIP = ip;
this.remotePort = port;
this.network = network;
this.userCount = userCount;
this.fileCount = fileCount;
this.fileSize = fileSize;
}
public Server(String host, int port, String network)
{
this(host, null, port, network, -1, -1, -1);
}
public Server(String host, int port)
{
this(host, port, "");
}
public Server()
{
this("", 8888);
}
//--- Method(s) ---
public boolean isConnected()
{
return (status == STATUS_CONNECTED);
}
public boolean isLoginCustomized()
{
return (username != null) || (password != null) || (email != null);
}
/**
* The server is accepting messages.
*/
public boolean isReady()
{
return (status == STATUS_CONNECTING
|| status == STATUS_CONNECTED);
}
protected String getStatusMsg(int index)
{
StringBuffer sb = new StringBuffer();
sb.append(STATUS_MSGS[index]);
if (index == STATUS_CONNECTED) {
if (getLocalPort() == 0) {
sb.append(" (firewalled)");
}
sb.append(" [");
sb.append(searchCount);
sb.append("]");
}
return sb.toString();
}
public IChannel create(String name)
{
Channel c = new Channel(this, name, "", 0);
channels.put(name, c);
return c;
}
public boolean equals(Object obj)
{
if (obj instanceof Server) {
Server s = (Server)obj;
return (getHost().equalsIgnoreCase(s.getHost())
&& getPort() == s.getPort());
}
return false;
}
public Channel getChannel(String name)
{
Channel c = (Channel)channels.get(name);
if (c == null) {
c = new Channel(this, name, "", 0);
channels.put(name, c);
}
return c;
}
public IChannel[] getChannels()
{
Object[] values = channels.values().toArray();
IChannel[] array = new IChannel[values.length];
System.arraycopy(values, 0, array, 0, array.length);
return array;
}
public int getFileCount()
{
return fileCount;
}
public int getFileSize()
{
return fileSize;
}
public String getEmail()
{
return (email != null) ? email : prefs.getEmail();
}
public void setEmail(String newValue)
{
if (newValue != null) {
try {
StringValidator.EMAIL.validate(newValue);
}
catch (IllegalArgumentException e) {
return;
}
}
email = newValue;
}
public String getHost()
{
return remoteHost;
}
public void setHost(String newValue)
{
remoteHost = newValue;
}
/**
* Napigator sends an ip which is sometimes more reliable than
* the hostname.
*/
public void setIP(String newValue)
{
remoteIP = newValue;
}
public String getIP()
{
//return remoteIP != null ? remoteIP : socket.getInetAddress().toString();
return remoteIP;
}
public long getLastLogin()
{
return lastLogin;
}
/**
* Returns the associated listener for incoming requests. Needed
* for reverse downloads.
*/
public NapListener getListener()
{
return listener;
}
public void setListener(NapListener newValue)
{
listener = newValue;
}
/**
* Returns port of listener or 0 if there is no listener running, which
* means XNap is behind a firewall.
*/
public int getLocalPort() {
return listener != null ? listener.getPort() : 0;
}
public String getName()
{
String network = getNetwork();
if (network.length() > 0) {
return network;
}
else {
return getHost();
}
}
public String getNetwork() {
return network;
}
public void setNetwork(String newValue)
{
network = newValue;
}
public String getPassword()
{
return (password != null) ? password : prefs.getPassword();
}
public void setPassword(String newValue)
{
if (newValue != null) {
try {
StringValidator.REGULAR_STRING.validate(newValue);
}
catch (IllegalArgumentException e) {
return;
}
}
password = newValue;
}
public int getPort()
{
return remotePort;
}
public void setPort(int newValue)
{
remotePort = newValue;
}
public String getRedirectedHost()
{
return redirectedHost;
}
public int getRedirectedPort()
{
return redirectedPort;
}
// public SendQueue getSendQueue()
// {
// return sendQueue;
// }
/**
* Returns the index range of shared files.
*/
public Range getShared()
{
return (Range)shared.clone();
}
public void setShared(Range newValue)
{
shared = (Range)newValue.clone();
}
public boolean isTemporary()
{
return temporay;
}
public boolean isRedirector()
{
return redirector;
}
public void setTemporary(boolean newValue)
{
temporay = newValue;
}
public User getUser(String nick)
{
User u = (User)users.get(nick);
if (u == null) {
u = new User(nick, this);
users.put(nick, u);
}
return u;
}
public IUser getUser()
{
return getUser(getUsername());
}
public int getUserCount()
{
return userCount;
}
public String getUsername()
{
return (username != null) ? username : prefs.getUsername();
}
public void setUsername(String newValue)
{
if (newValue != null) {
try {
StringValidator.REGULAR_STRING.validate(newValue);
}
catch (IllegalArgumentException e) {
return;
}
}
username = newValue;
}
public ServerVersion getVersion()
{
return version;
}
public void setVersion(String newValue)
{
version = new ServerVersion(newValue);
}
public void setRedirectedHost(String redirectedHost)
{
this.redirectedHost = redirectedHost;
}
public void setRedirectedPort(int redirectedPort)
{
this.redirectedPort = redirectedPort;
}
public void setRedirector(boolean newValue)
{
this.redirector = newValue;
}
/**
* Logs into server.
*/
public void login(boolean newUser)
{
if (!canStart()) {
return;
}
lastLogin = System.currentTimeMillis();
setStatus(STATUS_CONNECTING);
this.newUser = newUser;
// start main loop to listen for msgs
runner = new Thread(this, "OpenNapServer " + getHost());
runner.start();
}
/**
* Connects to a redirector server and reads the host information.
*
* @return true, if successful; false, if failed
*/
public boolean fetchHost(String host, int port) throws IOException
{
Socket socket = null;
InputStream in = null;
try {
socket = new Socket(host, port);
in = new BufferedInputStream(socket.getInputStream());
byte[] b = new byte[1024];
int c = in.read(b, 0, 1024);
if (c > 0) {
// chop the \n
String response = new String(b, 0, c - 1);
StringTokenizer t = new StringTokenizer(response, ":");
if (t.countTokens() == 2) {
try {
setRedirectedHost(t.nextToken());
setRedirectedPort
(Integer.parseInt(t.nextToken()));
return true;
}
catch (NumberFormatException e) {
}
}
}
}
finally {
try {
if (in != null) {
in.close();
}
if (socket != null) {
socket.close();
}
}
catch (IOException e) {
}
}
return false;
}
public void logout()
{
die(STATUS_NOT_CONNECTED);
}
public void messageReceived(String channelName, String nick, String message)
{
Channel c = (Channel)channels.get(channelName);
if (c != null) {
c.messageReceived(getUser(nick), message);
}
else {
StringBuffer sb = new StringBuffer();
sb.append(getHost());
sb.append(" (");
sb.append(channelName);
sb.append(") : ");
sb.append(message);
ChatManager.getInstance().globalMessageReceived(sb.toString());
}
}
public synchronized void addBrowse(Browse b) throws IOException
{
if (!isConnected()) {
throw new IOException("server disconnected");
}
browseQueue.add(b);
allowBrowse();
}
public synchronized void addSearch(Search s) throws IOException
{
if (!isConnected()) {
throw new IOException("server disconnected");
}
int i;
if (currentSearch != null && currentSearch.equals(s)) {
for (Iterator it = currentSearch.getResults().iterator();
it.hasNext();) {
s.add((SearchResult)it.next());
}
currentSearch.addPeer(s);
}
else if ((i = searchQueue.indexOf(s)) != -1) {
((Search)searchQueue.get(i)).addPeer(s);
}
else if (!useSearchCache(s)) {
searchQueue.add(s, s.getPriority());
allowSearch();
}
}
/**
* Sleeps until ready to send.
*/
public void waitUntilReadyToSend()
{
long tickLeft
= tickLength - (System.currentTimeMillis() - writeTickStart);
if (tickLeft <= 0) {
writeTickStart = System.currentTimeMillis();
writeTickPacketCount = 0;
}
else if (writeTickPacketCount >= maxPacketsPerTick) {
//logger.debug("flood protection: stalling for "
// + tickLeft + " ms");
try {
Thread.currentThread().sleep(tickLeft);
}
catch (InterruptedException e) {
}
}
}
public synchronized void removeBrowse(Browse b)
{
browseQueue.remove(b);
}
/**
* Is only called when search didn't take place or was aborted. That's
* why the cache entry is removed too.
*/
public synchronized void removeSearch(Search s)
{
searchQueue.remove(s);
}
public void send(ClientMessage msg) throws IOException
{
sendPacket(new Packet(msg.getType(), msg.getData(version)));
}
public void updateChannels() throws IOException
{
MessageHandler.send(this, new ListChannelsMessage());
}
private ServerMessage getNextMessage()
{
Packet p = null;
while (!die) {
try {
p = recvPacket();
return MessageFactory.create(this, p.id, p.data);
}
catch (InterruptedIOException e) {
}
catch (IOException e) {
logger.error(getHost() + ":" + getPort()
+ " getNextMessage: " + e.getMessage());
logger.debug(getHost() + ":" + getPort()
+ " getNextMessage ", e);
if (getStatus() == STATUS_CONNECTING) {
die(STATUS_LOGIN_FAILED, lastError);
}
else {
die(STATUS_FAILED, lastError);
}
}
catch (InvalidMessageException e) {
logger.error(getHost() + ":" + getPort()
+ " getNextMessage: " + p, e);
}
}
return null;
}
private byte[] read(int length) throws IOException
{
byte[] textBuf = new byte[length];
int read = 0;
while (read < length) {
try {
int c = in.read(textBuf, read, length - read);
if (c == -1) {
throw new IOException("Connection closed");
}
read += c;
}
catch (InterruptedIOException e) {
if (die) {
throw e;
}
}
}
return textBuf;
}
private Packet recvPacket() throws IOException
{
String content = "";
byte[] header = read(4);
// byte types are signed...
int lo = 0xFF & (int)header[0];
int hi = 0xFF & (int)header[1];
int length = 0xFFFF & (hi << 8 | lo);
lo = 0xFF & (int)header[2];
hi = 0xFF & (int)header[3];
int id = 0xFFFF & (hi << 8 | lo);
if (length > 0) {
content = new String(read(length));
}
logger.debug("< " + getHost() + ":" + getPort() + " " +
MessageFactory.getMessageName(id) + "(" +id + ") " + content);
return new Packet(id, content);
}
private void sendPacket(Packet p) throws IOException
{
logger.debug("> " + getHost() + ":" + getPort() + " " +
MessageStrings.getMessageName(p.id) + "(" + p.id + ") "
+ p.data);
// logger.debug("> " + getHost() + ":" + getPort() + " (" + p.id + ") "
// + p.data);
try {
int dataLength = p.data.getBytes().length;
byte[] data = new byte[2 + 2 + dataLength];
int type = (short)p.id;
int len = (short)dataLength;
data[0] = (byte)len;
data[1] = (byte)(len >> 8);
data[2] = (byte)type;
data[3] = (byte)(type >> 8);
System.arraycopy(p.data.getBytes(), 0,
data, 4, dataLength);
// maybe we should do some synchronization here
// we don't want to have multiple threads write half packets
out.write(data);
out.flush();
// flood protection
writeTickPacketCount++;
}
catch (NullPointerException e) {
logger.debug(getHost() + ":" + getPort() + " sendPacket " + p, e);
if (getStatus() == STATUS_CONNECTING) {
die(STATUS_LOGIN_FAILED, e.getMessage());
}
else {
die(STATUS_FAILED, e.getMessage());
}
throw(new IOException("Internal error"));
}
catch (IOException e) {
logger.debug(getHost() + ":" + getPort() + " sendPacket " + p, e);
if (getStatus() == STATUS_CONNECTING) {
die(STATUS_LOGIN_FAILED, e.getMessage());
}
else {
die(STATUS_FAILED, e.getMessage());
}
throw(e);
}
}
/**
* Opens the socket to the server and connects the streams.
* If host1 != null, it is tried first.
*
* @param host1 the ip address of the server or null if unknown
* @param host2 the hostname of the server
* @param port the port of the server
*/
private void connect(String host1, String host2, int port)
throws IOException
{
socket = null;
if (host1 != null) {
try {
socket = new Socket(host1, port);
}
catch (IOException e) {
setIP(null);
}
}
if (socket == null) {
for (int i = 0; i < MAX_RETRY_ON_CONNECTION_REFUSED
&& socket == null; i++) {
try {
socket = new Socket(host2, port);
}
catch (ConnectException e) {
if (i == MAX_RETRY_ON_CONNECTION_REFUSED - 1) {
throw e;
}
try {
Thread.sleep(10);
}
catch (InterruptedException e2) {
}
}
}
}
socket.setSoTimeout(SOCKET_TIMEOUT);
in = new BufferedInputStream(socket.getInputStream());
out = socket.getOutputStream();
setStatus(STATUS_CONNECTING, XNap.tr("logging in") + "...");
// server.setStateDescription(XNap.tr("logging in") + "...");
try {
if (newUser) {
send(new NickCheckMessage(getUsername()));
}
else {
// login(false);
MessageHandler.login(this, false);
}
}
catch (IOException e) {
die(STATUS_LOGIN_FAILED, XNap.tr("login failed"));
}
}
/**
* Waits for msgs from napster server and passes them on to the
* individual MsgQueues.
*/
public void run()
{
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
die = false;
currentBrowse = null;
currentSearch = null;
searchCount = 0;
lastError = null;
searchCache.clear();
users.clear();
try {
if (isRedirector()) {
if (fetchHost(getHost(), getPort())) {
connect(null, getRedirectedHost(),
getRedirectedPort());
}
else {
die(STATUS_FAILED, "invalid response");
}
}
else {
connect(remoteIP, getHost(), getPort());
}
}
catch (ConnectException e) {
// this is connection refused, try again later
// die(STATUS_FAILED, MessageFormat.format
// (XNap.tr("failed: {0}"),
// new Object[] { e.getLocalizedMessage() }));
die(STATUS_FAILED, XNap.tr("connection refused"));
}
catch (UnknownHostException e) {
die(STATUS_ERROR, XNap.tr("unknown host"));
}
catch (IOException e) {
// not route to host or something, probably unrecoverable
die(STATUS_ERROR, XNap.tr("error: {0}", e.getLocalizedMessage()));
}
while (!die) {
ServerMessage msg = getNextMessage();
if (die) {
break;
}
if (msg instanceof NickNotRegisteredMessage) {
if (getStatus() == STATUS_CONNECTING) {
try {
MessageHandler.login(this, true);
}
catch (IOException e) {
setStatus(STATUS_LOGIN_FAILED, e.getMessage());
}
}
}
else if (msg instanceof NickAlreadyRegisteredMessage) {
die(STATUS_LOGIN_FAILED, Plugin.tr("nick already registered"));
}
else if (msg instanceof InvalidNickMessage) {
die(STATUS_LOGIN_FAILED, Plugin.tr("invalid nick"));
}
else if (msg instanceof LoginAckMessage) {
if (getStatus() == STATUS_CONNECTING) {
//sendQueue.clear();
setStatus(STATUS_CONNECTED);
}
}
else if (msg instanceof LoginErrorMessage) {
die(STATUS_LOGIN_FAILED, ((LoginErrorMessage)msg).message);
}
else if (msg instanceof BrowseResponseMessage) {
if (currentBrowse != null) {
currentBrowse.add((BrowseResponseMessage)msg);
}
}
else if (msg instanceof EndBrowseMessage) {
if (currentBrowse != null) {
currentBrowse.finished();
currentBrowse = null;
updateStatus();
}
}
else if (msg instanceof SearchResponseMessage) {
if (currentSearch != null) {
currentSearch.add((SearchResponseMessage)msg);
}
}
else if (msg instanceof EndSearchMessage) {
if (currentSearch != null) {
synchronized (this) {
currentSearch.finished();
// create cache entry if auto download search
if (currentSearch.getPriority()
== ISearch.PRIORITY_BACKGROUND) {
searchCache.put(currentSearch.getRequest(),
currentSearch.getResults());
}
currentSearch = null;
}
updateStatus();
}
}
else if (msg instanceof ServerStatsMessage) {
ServerStatsMessage ssm = (ServerStatsMessage)msg;
userCount = ssm.userCount;
fileCount = ssm.fileCount;
fileSize = ssm.fileSize;
fireStatusChange(status, STATUS_NEW_STATS);
}
else if (msg instanceof ErrorMessage) {
lastError = ((ErrorMessage)msg).message;
if (lastError.startsWith("You have been killed by server")
|| lastError.startsWith("You were killed by server"))
{
setStatus(STATUS_ERROR, lastError);
}
}
MessageHandler.handle(msg);
allowSearch();
allowBrowse();
}
try {
if (in != null)
in.close();
if (out != null)
out.close();
if (socket != null)
socket.close();
}
catch (IOException e) {
}
synchronized (this) {
if (currentBrowse != null) {
currentBrowse.failed(Plugin.tr("Server disconnected"));
}
for (Iterator i = browseQueue.iterator(); i.hasNext();) {
((Browse)i.next()).failed(Plugin.tr("Server disconnected"));
}
browseQueue.clear();
}
synchronized (this) {
if (currentSearch != null) {
currentSearch.failed(Plugin.tr("Server disconnected"));
}
Object[] searches = searchQueue.toArray();
for (int i = 0; i < searches.length; i++) {
((Search)searches[i]).failed(Plugin.tr("Server disconnected"));
}
searchQueue.clear();
}
// close all open chat channels
for (Iterator i = channels.values().iterator(); i.hasNext();) {
((IChannel)i.next()).close();
}
logger.debug(toString() + " has died");
died();
}
private void updateStatus()
{
if (status == STATUS_CONNECTED) {
StringBuffer sb = new StringBuffer(getStatusMsg(status));
if (currentSearch != null) {
sb.append(", searching");
}
if (searchQueue.size() > 0) {
sb.append(", ");
sb.append(searchQueue.size());
sb.append(" searches pending");
}
if (currentBrowse != null) {
sb.append(", browsing");
}
setStatus(status, sb.toString());
}
}
public String toString()
{
StringBuffer sb = new StringBuffer();
sb.append(getHost());
sb.append(":");
sb.append(getPort());
String network = getNetwork();
if (network != null && !network.equals("")) {
sb.append(" (");
sb.append(network);
sb.append(")");
}
return sb.toString();
}
private synchronized void allowBrowse()
{
if (currentBrowse != null || browseQueue.size() == 0) {
return;
}
currentBrowse = (Browse)browseQueue.removeFirst();
currentBrowse.start();
updateStatus();
}
private synchronized void allowSearch()
{
if (currentSearch != null || searchQueue.size() == 0) {
return;
}
Search s = (Search)searchQueue.peek();
if (useSearchCache(s)) {
searchQueue.pop();
return;
}
searchQueue.pop();
currentSearch = s;
lastSearch = System.currentTimeMillis();
searchCount++;
currentSearch.start();
updateStatus();
}
private synchronized boolean useSearchCache(Search s)
{
if (s.getPriority() < ISearch.PRIORITY_USER) {
searchCache.purge();
LinkedList results = searchCache.get(s.getRequest());
if (results != null) {
// cache hit
logger.debug("nap server: reuse of searchresults");
for (Iterator i = results.iterator(); i.hasNext();) {
SearchResult sr = (SearchResult)i.next();
s.add(sr);
}
s.close();
return true;
}
}
return false;
}
}