/*
* 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.io.ThrottledInputStream;
import xnap.net.*;
import xnap.plugin.nap.Plugin;
import xnap.plugin.nap.net.msg.*;
import xnap.plugin.nap.net.msg.client.*;
import xnap.plugin.nap.net.msg.server.AcceptFailedMessage;
import xnap.plugin.nap.net.msg.server.DownloadAckMessage;
import xnap.plugin.nap.net.msg.server.ErrorMessage;
import xnap.plugin.nap.net.msg.server.GetErrorMessage;
import xnap.plugin.nap.net.msg.server.MessageListener;
import xnap.plugin.nap.net.msg.server.MessageStream;
import xnap.plugin.nap.net.msg.server.QueueLimitMessage;
import xnap.plugin.nap.net.msg.server.ServerMessage;
import xnap.plugin.nap.util.NapPreferences;
import xnap.util.*;
import java.io.*;
import java.net.*;
import java.util.*;
import org.apache.log4j.Logger;
public class Download extends AbstractDownload
implements MessageListener, ExceptionListener
{
// --- Constant(s) ---
/**
* Server needs to send download ack.
*/
public static final int SERVER_TIMEOUT = 2 * 60 * 1000;
/**
* Socket timeout during connect.
*/
public static final int CONNECT_TIMEOUT = 1 * 60 * 1000;
/**
* Requests are sent in this interval if this download is remotely queued.
*/
public static final int QUEUED_REQUEST_INTERVAL = 5 * 60 * 1000;
/**
* Requests are sent in this interval if this download cound not be
* remotely queued.
*/
public static final int REQUEST_INTERVAL = 2 * 60 * 1000;
/**
* Do not send //WantQueue more times than this. Once should be enough
* anyway.
*/
public static final int MAX_WANT_QUEUE = 2;
// --- Data Field(s) ---
protected static Logger logger = Logger.getLogger(Download.class);
private SearchResult sr;
private Server server;
protected Socket socket;
protected InputStream in;
protected OutputStream out;
protected long offset;
protected int port;
protected String ip;
private boolean subscribed = false;
private boolean requestSent = false;
private long lastRequestSent = 0;
private int wantQueueCount = 0;
// --- Constructor(s) ---
public Download(SearchResult sr)
{
super(sr.getUser(), sr.getFilesize());
this.sr = sr;
this.server = sr.getNapUser().getServer();
// send whois query to update clientInfo, in case we need it later
sr.getNapUser().update();
}
// --- Method(s) ---
public int available() throws IOException
{
return in.available();
}
public void close(boolean sendMsg)
{
if (sendMsg) {
MessageHandler.send(new DownloadCompleteMessage());
}
try {
if (socket != null)
socket.close();
if (in != null)
in.close();
if (out != null)
out.close();
}
catch (IOException e) {
}
}
public void close()
{
close(true);
}
public boolean connect(long offset) throws IOException
{
if (requestSent) {
requestSent = false;
sr.getNapUser().incQueuedCount(-1);
}
// we are not queued anymore
setQueuePos(-1);
wantQueueCount = 0;
start();
this.offset = offset;
if (port == 0) {
if (!establishReverseStream()) {
return false;
}
}
else {
establishStream(ip, port);
}
in = new ThrottledInputStream(in);
MessageHandler.send(new DownloadingFileMessage());
return true;
}
public void dequeue()
{
if (requestSent) {
requestSent = false;
sr.getNapUser().incQueuedCount(-1);
}
wantQueueCount = 0;
lastRequestSent = 0;
unsubscribe(this);
}
public void exceptionThrown(Exception e)
{
getParent().remove(this);
}
public void enqueue(IDownloadContainer parent)
{
setParent(parent);
subscribe(this);
boolean allowed = sr.getNapUser().isAllowedToRequestDownload();
if (allowed || requestSent) {
if (!requestSent) {
// after we have sent the request, we might not be allowed
// anymore because the queue count was increased therefore
// requestSent is set to true
requestSent = true;
sr.getNapUser().incQueuedCount(1);
}
sendDownloadRequest(false);
}
else if (!allowed && requestSent) {
// we are not allowed anymore
requestSent = false;
sr.getNapUser().incQueuedCount(-1);
}
lastTry = System.currentTimeMillis();
}
private int getRequestInterval()
{
// nap does not support remote queueing
String client = sr.getUser().getClientInfo();
if ((client != null && client.startsWith("nap"))
|| getQueuePos() <= 0) {
return REQUEST_INTERVAL;
}
else {
return QUEUED_REQUEST_INTERVAL;
}
}
private void sendDownloadRequest(boolean force)
{
if (force || (System.currentTimeMillis() - lastRequestSent
> getRequestInterval())) {
DownloadRequestMessage ms = new DownloadRequestMessage
(sr.getUser().getName(), sr.getFilename());
ms.setExceptionListener(this);
MessageHandler.send(server, ms);
lastRequestSent = System.currentTimeMillis();
}
}
/**
* Opens socket and requests file.
*/
private void establishStream(String ip, int port) throws IOException
{
logger.debug("opening socket " + ip + ":" + port);
//socket = new Socket(ip, port);
socket = NetHelper.connect(ip, port, CONNECT_TIMEOUT);
try {
socket.setSoTimeout(CONNECT_TIMEOUT);
}
catch (SocketException s) {
}
out = socket.getOutputStream();
in = new BufferedInputStream(socket.getInputStream());
// read magic number '1'
logger.debug("reading magic number");
char c = (char)in.read();
if (c != '1') {
throw new IOException(Plugin.tr("Invalid request"));
}
// get request needs to be split over 2 packets
String message = "GET";
out.write(message.getBytes());
out.flush();
message = server.getUsername() + " "
+ "\"" + sr.getFilename() + "\"" + " " + offset;
logger.debug("the whole request: " + message);
out.write(message.getBytes());
out.flush();
String expected = sr.getFilesize() + "";
StringBuffer sb = new StringBuffer();
while (sb.length() < expected.length()) {
int b = in.read();
if (b == -1) {
throw new IOException(Plugin.tr("Socket error"));
}
c = (char)b;
if (Character.isDigit(c)) {
// ignore leading zeros
if (!(sr.getFilesize() != 0 && sb.length() == 0 && c == '0')) {
sb.append(c);
}
}
else if (c == 'F') {
throw new IOException(Plugin.tr("File not shared"));
}
else {
throw new IOException(Plugin.tr("Invalid request"));
}
}
logger.debug("file length: " + sb.toString());
if (Long.parseLong(sb.toString()) != sr.getFilesize()) {
throw new IOException(Plugin.tr("Filesizes did not match"));
}
}
/**
* Waits for push.
*/
private boolean establishReverseStream() throws IOException
{
if (server.getLocalPort() == 0) {
throw new IOException(Plugin.tr("Both parties firewalled"));
}
MessageStream ms = new MessageStream(server);
MessageHandler.subscribe(ErrorMessage.TYPE, ms);
server.send(new AltDownloadRequestMessage(sr.getUser().getName(),
sr.getFilename()));
DownloadSocket ds = new DownloadSocket
(sr.getUser().getName(), sr.getFilename(), sr.getFilesize());
checkForError(ms);
if (getQueuePos() > 0) {
return false;
}
// FIX: we might get queued in the mean time
ds = (DownloadSocket)server.getListener().waitForSocket
(ds, CONNECT_TIMEOUT);
checkForError(ms);
MessageHandler.unsubscribe(ErrorMessage.TYPE, ms);
if (ds == null) {
if (getQueuePos() >= 0) {
return false;
}
else {
throw new IOException(Plugin.tr("Listener timeout"));
}
}
logger.debug("reverse stream socket established");
socket = ds.socket;
try {
socket.setSoTimeout(CONNECT_TIMEOUT);
}
catch (SocketException e) {
}
out = socket.getOutputStream();
in = ds.in;
// write offset
out.write((new Long(offset)).toString().getBytes());
out.flush();
return true;
}
private void checkForError(MessageStream ms) throws IOException
{
while (ms.hasNext()) {
ServerMessage m = (ServerMessage)ms.next();
if (m instanceof ErrorMessage) {
String expect = sr.getUser().getName() + " is not firewalled";
if (((ErrorMessage)m).message.equals(expect)) {
MessageHandler.unsubscribe(ErrorMessage.TYPE, ms);
throw new IOException(Plugin.tr("User is not firewalled"));
}
}
}
}
/**
* Handles messages received from the server as response to the download
* request. Notifies the parent download container if download is ready to
* start or if it should be removed due to an error message received from
* the server.
*/
public void messageReceived(ServerMessage m)
{
if (m instanceof FilenameMessage &&
((FilenameMessage)m).getFilename().equals(sr.getFilename())
&& m.getServer() == server) {
// this msg is only for us
m.consume();
}
else {
// not for us
return;
}
// try to find out, what kind of message we have received
if (m instanceof QueueLimitMessage) {
int pos = ((QueueLimitMessage)m).maxDownloads;
if (pos == 0 || pos >= 10000) {
setQueuePos(0);
if (sendWantQueue()) {
sendDownloadRequest(true);
}
}
else if (pos > 0 && pos < 10000) {
setQueuePos(pos);
}
else {
setQueuePos(-1);
}
logger.debug(sr.getUser().getName() + " has queued us at pos "
+ getQueuePos());
}
else if (m instanceof AcceptFailedMessage
|| m instanceof GetErrorMessage) {
logger.debug(sr.getUser().getName()
+ " telling parent to remove " + sr.getFilename());
getParent().remove(this);
}
else if (m instanceof DownloadAckMessage) {
DownloadAckMessage dam = (DownloadAckMessage)m;
ip = dam.ip;
port = dam.port;
logger.debug("telling parent to start me " + ip + ":" + port + " "
+ this);
getParent().start(this);
}
}
public int read(byte[] b, int offset, int length) throws IOException
{
return in.read(b, offset, length);
}
/**
* Returns true, if a download request should be sent.
*/
public boolean sendWantQueue()
{
if (wantQueueCount >= MAX_WANT_QUEUE) {
// avoid sending to many requests, some clients might not
// queue us at all or return 0 as a valid queue position
return false;
}
// some clients might obfuscate thier client info so we might fail
// to recognize them as WinMX 2.6, therefore we send WantQueues
// to them
String client = sr.getUser().getClientInfo();
boolean send = false;
if (client != null) {
send |= client.startsWith("WinMX v2.6");
send |= client.startsWith("Napigator");
send |= client.startsWith("TrippyMX");
send |= client.startsWith("Utatane");
send |= wantQueueCount > 1 && client.startsWith("WinMX");
}
if (send) {
logger.debug("Sending //WantQueue to " + sr.getUser().getName()
+ " who uses client " + client);
MessageHandler.send(server, new PrivateMessage
(sr.getUser().getName(), "//WantQueue"));
}
wantQueueCount++;
return true;
}
/**
* Subscribes to all messages relevant for downloading.
*/
private void subscribe(MessageListener ms)
{
if (subscribed) {
return;
}
MessageHandler.subscribe(DownloadAckMessage.TYPE, ms);
MessageHandler.subscribe(QueueLimitMessage.TYPE, ms);
MessageHandler.subscribe(GetErrorMessage.TYPE, ms);
MessageHandler.subscribe(AcceptFailedMessage.TYPE, ms);
subscribed = true;
}
/**
* Unsubscribes from all messages relevant for downloading.
*/
private void unsubscribe(MessageListener ms)
{
if (!subscribed) {
return;
}
MessageHandler.unsubscribe(DownloadAckMessage.TYPE, ms);
MessageHandler.unsubscribe(QueueLimitMessage.TYPE, ms);
MessageHandler.unsubscribe(GetErrorMessage.TYPE, ms);
MessageHandler.unsubscribe(AcceptFailedMessage.TYPE, ms);
subscribed = false;
}
public String toString()
{
return "download " + sr.getFilename() + "@" + sr.getUser().getName();
}
}