/**
* Copyright (c) 2007, Markus Jevring <markus@jevring.net>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The names of the contributors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/
package cu.ftpd.commands.transfer;
import cu.ftpd.Connection;
import cu.ftpd.Server;
import cu.ftpd.ServiceManager;
import cu.ftpd.events.Event;
import cu.ftpd.events.EventFactory;
import cu.ftpd.filesystem.FileSystem;
import cu.ftpd.filesystem.Section;
import cu.ftpd.filesystem.filters.ForbiddenFilesFilter;
import cu.ftpd.filesystem.permissions.ActionPermission;
import cu.ftpd.filesystem.permissions.SpeedPermission;
import cu.ftpd.logging.Logging;
import cu.ftpd.user.User;
import cu.ftpd.user.UserPermission;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
/**
* @author Markus Jevring <markus@jevring.net>
* @since 2008-jan-09 - 10:26:17
* @version $Id: CommandRETR.java 305 2011-02-07 18:33:18Z jevring $
*/
public class CommandRETR implements TransferController {
private final Connection connection;
private final FileSystem fs;
private final User user;
private final String filename;
private final boolean encryptedDataConnection;
private final boolean sscn;
private final boolean fastAsciiTransfer;
private boolean semaphoreTaken;
private FileInputStream in;
private TransferThread transfer;
private Section section;
private Socket dataConnection;
private File file;
private static String[] freeFiles;
private InetAddress remoteHost;
public CommandRETR(Connection connection, FileSystem fs, User user, String filename, boolean encryptedDataConnection, boolean sscn, boolean fastAsciiTransfer) {
this.connection = connection;
this.fs = fs;
this.user = user;
this.filename = filename;
this.encryptedDataConnection = encryptedDataConnection;
this.sscn = sscn;
this.fastAsciiTransfer = fastAsciiTransfer;
}
public void start() {
connection.dataConnectionSemaphore.acquireUninterruptibly();
try {
// note: we must collect the data connection here, because if we don't, then it will be null when we close due to an error, but won't be closed in Connection
dataConnection = connection.getDataConnection();
// due to the fact that getInetAddress() can return null if we are not connected, we have to do a little special thing here, which can cause irregularities in the xferlog, but so be it
remoteHost = dataConnection.getInetAddress();
// try semaphores first, as the other way around never makes privileged users count towards the max for unprivileged users, effectively dividing them into two groups
if ((semaphoreTaken = Server.getInstance().getDownloadSemaphore().tryAcquire()) || user.hasPermission(UserPermission.PRIVILEGED)) {
try {
file = fs.resolveFile(filename);
String ftpPathToFile = FileSystem.resolvePath(file);
// NOTE: the FileInputStream will take care of throwing an exception if the file turns out to be a dir
if (file.exists()) {
if (ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.DOWNLOAD, ftpPathToFile, user)) {
if (!ForbiddenFilesFilter.getForbiddenFiles().contains(filename)) {
if (user.hasLeech() || file.length() < user.getCredits()) {
if (remoteHost != null) {
// if we have an address, and we are either transferring data to the same host that we are connecting from, or we have permission to FXPUP, then continue
if (remoteHost.equals(connection.getClientHost()) || ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.FXPDOWNLOAD, ftpPathToFile, user)) {
Event event = EventFactory.download(user, fs, file, remoteHost, 0, 0, "PENDING");
boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, connection);
if (proceed) {
section = fs.getSection(file);
in = new FileInputStream(file);
createTransfer();
connection.respond("150 Opening " + fs.getType() + " mode data connection for RETR command" + (encryptedDataConnection ? " with SSL/TLS encryption." : "."));
connection.setControlConnectionTimeout(0);
transfer.start();
} else {
close();
// Note: we don't send anything to the connection here, because that is the job of the event handler
// when no handlers are registered, proceed will always be 'true'
}
} else {
close();
connection.respond("531 You do not have permission to download to another host than the one you are connecting from (FXPDOWN)");
}
} else {
close();
connection.respond("500 Remote host unknown, probably due to a closed data connection, transfer failed.");
}
} else {
close();
connection.respond("500 Insufficient credits, transfer aborted.");
}
} else {
close();
connection.respond("531 Forbidden.");
}
} else {
close();
connection.respond("531 Permission denied.");
}
} else {
close();
connection.respond("550 File not found: " + FileSystem.resolvePath(file));
}
} catch (IOException e) {
close();
connection.respond("500 " + e.getMessage());
}
} else {
close();
connection.respond("553 Too many users downloading at the moment.");
}
} catch (IllegalStateException e) {
connection.respond("500 " + e.getMessage());
connection.dataConnectionSemaphore.release();
}
}
public long getSpeed() {
if (transfer != null) {
return transfer.getCurrentSpeed();
} else {
return 0;
}
}
private void createTransfer() throws IOException {
if (encryptedDataConnection) {
((javax.net.ssl.SSLSocket)dataConnection).setUseClientMode(sscn);
}
if ("ASCII".equals(fs.getType())) {
if (fastAsciiTransfer) {
transfer = new CharacterTransferThread(this, new InputStreamReader(in, "ISO-8859-1"), new BufferedWriter(new OutputStreamWriter(dataConnection.getOutputStream(), "ISO-8859-1")));
} else {
transfer = new TranslatingCharacterTransferThread(this, new BufferedReader(new InputStreamReader(in, "ISO-8859-1")), new BufferedWriter(new OutputStreamWriter(dataConnection.getOutputStream(), "ISO-8859-1")), false);
}
} else {
if (fs.getOffset() > 0) {
in.skip(fs.getOffset()); // it's native, so it'll be fast
}
long limit = ServiceManager.getServices().getPermissions().getLimit(user, FileSystem.resolvePath(file.getParentFile()), SpeedPermission.DOWNLOAD);
if (limit <= 0) {
transfer = new ByteTransferThread(this, in, dataConnection.getOutputStream());
} else {
transfer = new SpeedLimitingByteTransferThread(this, in, dataConnection.getOutputStream(), limit);
}
}
}
public void error(Exception e, long bytesTransferred, long transferTime) {
close();
log(bytesTransferred, transferTime);
connection.reportTransferFailure(e.getMessage());
Event event = EventFactory.download(user, fs, file, remoteHost, bytesTransferred, transferTime, "FAILED");
ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
}
public void complete(long bytesTransferred, long transferTime) {
close();
log(bytesTransferred, transferTime);
connection.respond("226- " + fs.getType() + " transfer of " + filename + " completed.");
connection.statline(transfer.getSpeed());
Event event = EventFactory.download(user, fs, file, remoteHost, bytesTransferred, transferTime, "COMPLETE");
ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
}
private void log(long bytesTransferred, long transferTime) {
if (!user.hasLeech() && !isFreeFile(filename)) {
user.takeCredits(bytesTransferred);
}
ServiceManager.getServices().getUserStatistics().download(user.getUsername(), section.getName(), bytesTransferred, transferTime);
}
private void close() {
if (semaphoreTaken) {
Server.getInstance().getDownloadSemaphore().release();
}
fs.rest(0);
if (in != null) {
try {
in.close();
} catch (IOException e) {
Logging.getErrorLog().reportException("Failed to close file input stream", e);
//e.printStackTrace();
}
}
if (dataConnection != null) {
try {
dataConnection.close();
} catch (IOException e) {
Logging.getErrorLog().reportException("Failed to close data connection", e);
//e.printStackTrace();
}
}
// this ensures that, if a connection is broken, the timeout for the control connection is reset, so that it, too, can timeout
connection.resetControlConnectionTimeout();
connection.dataConnectionSemaphore.release();
}
public boolean isRunning() {
return transfer != null && transfer.isAlive();
}
private static boolean isFreeFile(String filename) {
if (freeFiles == null) {
return false;
} else {
for (int i = 0; i < freeFiles.length; i++) {
String freeFile = freeFiles[i];
if (filename.matches(freeFile)) {
//System.out.println(filename + " matches the free_file pattern " + freeFile);
return true;
}
}
return false;
}
}
private static String[] createFreeFiles(String freeFilesString) {
String[] freeFiles = null;
if (freeFilesString != null) {
freeFiles = freeFilesString.split(" ");
for (int i = 0; i < freeFiles.length; i++) {
freeFiles[i] = freeFiles[i].replace(".","\\.").replace("*",".*");
}
}
return freeFiles;
}
public static void initialize(String freeFilesString) {
freeFiles = createFreeFiles(freeFilesString);
}
}