/**
* 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.Server;
import cu.ftpd.Connection;
import cu.ftpd.ServiceManager;
import cu.ftpd.events.Event;
import cu.ftpd.events.EventFactory;
import cu.shell.ProcessResult;
import cu.ftpd.logging.Logging;
import cu.ftpd.filesystem.FileSystem;
import cu.ftpd.filesystem.Section;
import cu.ftpd.filesystem.filters.ForbiddenFilesFilter;
import cu.ftpd.filesystem.permissions.SpeedPermission;
import cu.ftpd.filesystem.permissions.ActionPermission;
import cu.ftpd.user.User;
import cu.ftpd.user.UserPermission;
import java.nio.channels.FileLock;
import java.io.*;
import java.net.Socket;
import java.net.InetAddress;
import java.util.zip.CheckedOutputStream;
import java.util.zip.CRC32;
/**
* @author Markus Jevring <markus@jevring.net>
* @since 2008-jan-08 - 21:13:17
* @version $Id: CommandSTOR.java 300 2010-03-09 20:48:19Z jevring $
*/
public class CommandSTOR implements TransferController {
private TransferThread transfer;
private FileLock lock;
private boolean semaphoreTaken = false;
private FileOutputStream fos;
private Socket dataConnection;
private RandomAccessFile raf;
private final Connection connection;
private File file;
private final FileSystem fs;
private final User user;
private Section section;
private final String filename;
private final boolean append;
private final boolean encryptedDataConnection;
private final boolean sscn;
private final CRC32 crc = new CRC32();
private final boolean onTheFlyCrc;
private InetAddress remoteHost;
private final boolean fastAsciiTransfer;
public CommandSTOR(Connection connection, FileSystem fs, User user, String filename, boolean append, boolean encryptedDataConnection, boolean sscn, boolean onTheFlyCrc, boolean fastAsciiTransfer) {
this.connection = connection;
this.fs = fs;
this.user = user;
this.filename = filename;
this.append = append;
this.encryptedDataConnection = encryptedDataConnection;
this.sscn = sscn;
this.onTheFlyCrc = onTheFlyCrc;
this.fastAsciiTransfer = fastAsciiTransfer;
}
public void start() {
// NOTE: while not necessary, we will take the precaution of closing the socket if anything goes wrong here.
connection.dataConnectionSemaphore.acquireUninterruptibly();
//synchronized (connection.dataConnectionLock) {
// 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
try {
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();
if ((semaphoreTaken = Server.getInstance().getUploadSemaphore().tryAcquire()) || user.hasPermission(UserPermission.PRIVILEGED)) {
try {
if (filename != null) {
file = fs.resolveFile(filename);
} else {
file = fs.createUniqueFile(user.getUsername());
}
String ftpPathToFile = FileSystem.resolvePath(file);
// _todo: ensure that the file is a file and not a directory
// can't really do that here, since the file doesn't exist
// NOTE: the FileOutputStream will take care of throwing an exception if the file turns out to be a dir
if (ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.UPLOAD, ftpPathToFile, user)) {
if (!ForbiddenFilesFilter.getForbiddenFiles().contains(file.getName())) {
if (file.getParentFile().canWrite()) {
if (!file.exists() || /* file.exists() && */ hasModifyPermission(file) ) {
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.FXPUPLOAD, ftpPathToFile, user)) {
Event event = EventFactory.upload(user, fs, file, remoteHost, 0, 0, null, "PENDING");
boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, connection);
//boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(new FileTransferEvent(Event.UPLOAD, user, fs.getRealParentWorkingDirectory(), FileTransferEvent.PENDING, 0, 0, file, fs.getType(), (remoteHost == null ? "n/a - connection failure" : remoteHost.getHostAddress())), connection);
if (proceed) {
// NOTE: dupecheck is handled as an event handler
//dupecheck();
section = fs.getSection(file);
ServiceManager.getServices().getMetadataHandler().setOwnership(file, user.getUsername(), user.getPrimaryGroup());
connection.setControlConnectionTimeout(0);
createTransfer();
if (filename == null) {
connection.respond("150 FILE: " + file.getName());
} else {
connection.respond("150 Opening " + fs.getType() + " mode data connection for STOR command" + (encryptedDataConnection ? " with SSL/TLS encryption." : "."));
}
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 upload to another host than the one you are connecting from (FXPUPLOAD)");
}
} else {
close();
connection.respond("500 Remote host unknown, probably due to a closed data connection, transfer failed.");
}
} else {
close();
connection.fileExists();
}
} else {
close();
connection.respond("553 Not allowed to upload in a write-protected directory.");
}
} else {
close();
connection.respond("531 Forbidden filename.");
}
} else {
close();
connection.respond("531 Permission denied.");
}
/*
} catch (IllegalPathException e) {
close();
connection.respond("500 " + e.getMessage());
} catch (PermissionException e) {
close();
connection.respond("553 " + e.getMessage());
*/
} catch (FileNotFoundException e) {
close();
connection.respond("500 Error creating file: " + FileSystem.resolvePath(file));
} catch (IOException e) {
close();
connection.respond("500 " + e.getMessage());
}
} else {
close();
connection.respond("553 Too many users uploading at the moment.");
// we don't close the socket here, since the client can just wait and send a new transfer command and utilize the old dataConnection.
}
} catch (IllegalStateException e) {
close();
connection.respond("500 " + e.getMessage());
}
//}
/*
get semaphore or privileged
get file
check write permissions in dir (specified, not pwd)
check file exists + overwrite permission
dupecheck
enter directory (file.getParentFile()), this file needs to always be != null, which it will be even when we do STOU, since it will be resolved to a newly created file
get Section from file
set otf crc
set append
set offset
set ownership
set control connection timeout to 0
get file lock
get FileOutputStream (or RandomAccessFile) // save this for last
get SocketInputStream
set client mode
send 150
start transfer
on error -> close()
*/
}
private void createTransfer() throws IOException {
if (encryptedDataConnection) {
((javax.net.ssl.SSLSocket)dataConnection).setUseClientMode(sscn);
}
long limit = ServiceManager.getServices().getPermissions().getLimit(user, FileSystem.resolvePath(file.getParentFile()), SpeedPermission.UPLOAD);
if (fs.getOffset() > 0) {
// RAF
raf = new RandomAccessFile(file, "rw");
takeLock(raf);
raf.seek(fs.getOffset());
// if we're here, we can't be in ascii mode anyway, so there's no use to check
if (limit <= 0) {
transfer = new RandomAccessFileTransferThread(this, dataConnection.getInputStream(), raf);
} else {
transfer = new SpeedLimitedRandomAccessFileTransferThread(this, dataConnection.getInputStream(), raf, limit);
}
} else {
// FOS
fos = new FileOutputStream(file, append);
takeLock(fos);
if ("ASCII".equals(fs.getType())) {
if (fastAsciiTransfer) {
transfer = new CharacterTransferThread(this, new InputStreamReader(dataConnection.getInputStream(), "ISO-8859-1"), new BufferedWriter(new OutputStreamWriter(fos, "ISO-8859-1")));
} else {
transfer = new TranslatingCharacterTransferThread(this, new BufferedReader(new InputStreamReader(dataConnection.getInputStream(), "ISO-8859-1")), new BufferedWriter(new OutputStreamWriter(fos, "ISO-8859-1")), true);
}
} else {
OutputStream out = fos;
if (!append && onTheFlyCrc) {
out = new CheckedOutputStream(fos, crc);
}
//System.out.println("STOR: " + dataConnection);
if (limit <= 0) {
transfer = new ByteTransferThread(this, dataConnection.getInputStream(), out);
} else {
transfer = new SpeedLimitingByteTransferThread(this, dataConnection.getInputStream(), out, limit);
}
}
}
}
public void error(Exception e, long bytesTransferred, long transferTime) {
/*
close()
report transfer failure (e.getMessage())
*/
close();
// _todo: make transferlog a handled event (maybe then we need to trigger on FAILURE as well)(or just leave transferlog to the internal machinery)
// (if so, then we can probably drop a lot of data from the FileTransferEvent)
// since it happens both at failure and and success, and because it is such an integral part of the service, the xferlog stays here
// xferlog(bytesTransferred, transferTime);
ProcessResult pcr = postProcess(bytesTransferred, transferTime);
connection.reportTransferFailure(e.getMessage());
Event event = EventFactory.upload(user, fs, file, remoteHost, bytesTransferred, transferTime, pcr, "FAILED");
ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
}
public void complete(long bytesTransferred, long transferTime) {
close();
// since it happens both at failure and and success, and because it is such an integral part of the service, the xferlog stays here
// xferlog(bytesTransferred, transferTime);
ProcessResult pcr = postProcess(bytesTransferred, transferTime);
connection.reply(226, pcr.message, true); // if this wasn't a race, the raceMessage will be null, and .reply(..) will print nothing
connection.respond("226- " + fs.getType() + " transfer of " + filename + " complete.");
connection.statline(transfer.getSpeed());
Event event = EventFactory.upload(user, fs, file, remoteHost, bytesTransferred, transferTime, pcr, "COMPLETE");
ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
}
private ProcessResult postProcess(long bytesTransferred, long transferTime) {
ProcessResult pcr = ServiceManager.getServices().getTransferPostProcessor().process(file, crc.getValue(), bytesTransferred, transferTime, user, section.getName(), transfer.getSpeed());
if (pcr.exitvalue == ProcessResult.OK.exitvalue) {
// the zipscript tells us that it was either ok or not handled. in both cases, we should add to dupelog and all other suff
log(bytesTransferred, transferTime);
} else {
// since the zipscript failed, we should delete the file
file.delete();
// we should still return whatever the zipscript said
}
return pcr;
}
private void log(long bytesTransferred, long transferTime) {
// _todo: move to a post-upload event handler?
// No, since we need to access it from so many other places anyway, it doesn't make sense. Besides, it is quite integral
// ok, since the download is called on normal download too
ServiceManager.getServices().getUserStatistics().upload(user.getUsername(), section.getName(), bytesTransferred, transferTime);
// ok since it's only called on successful transfers
if (!user.hasLeech()) {
user.giveCredits(bytesTransferred * section.getRatio());
}
}
private void close() {
if (semaphoreTaken) {
Server.getInstance().getUploadSemaphore().release();
}
fs.rest(0);
if (lock != null && lock.isValid()) {
try {
lock.release();
} catch (IOException e) {
Logging.getErrorLog().reportException("Failed to release lock", e);
//e.printStackTrace();
}
}
if (dataConnection != null) {
try {
dataConnection.close();
} catch (IOException e) {
Logging.getErrorLog().reportException("Failed to close data connection", e);
//e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
Logging.getErrorLog().reportException("Failed to close output stream", e);
//e.printStackTrace();
}
// we don't need to close the channel explicitly, because closing the stream closes the channel
}
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
Logging.getErrorLog().reportException("Failed to close random access file", e);
//e.printStackTrace();
}
// we don't need to close the channel explicitly, because closing the raf closes the channel
}
// this ensures that, if a connection is broken, the timeout for the control connection is reset, so that it, too, can timeout
connection.resetControlConnectionTimeout();
// we can't release this here, since we handle it differently if there is an error of if the transfer was succesful
connection.dataConnectionSemaphore.release();
// maybe we can. we need to, since it can fail with a dupe or something, after which we can't create data connections anymore unless it is released
// It DOES work, and it works great in RETR too
}
private void takeLock(FileOutputStream out) throws IOException {
try {
// I wanted to use the same locking mechanism for all three transfers, but apparently that wasn't possible.
// DAMN IT, why is there no generic way of obtaining a lock!
lock = out.getChannel().tryLock();
} catch (java.nio.channels.OverlappingFileLockException e) {
// NOTE: If this happens, 'lock' will be 'null' below anyway, so the error will still occur.
}
if (lock == null) {
// this means that someone else holds the lock already, which means that we abort
// NOTE: 'fos' will be closed in close();
throw new IOException("File is already being uploaded: Could not acquire exclusive lock on file");
}
}
private void takeLock(RandomAccessFile raf) throws IOException {
try {
// I wanted to use the same locking mechanism for all three transfers, but apparently that wasn't possible.
// DAMN IT, why is there no generic way of obtaining a lock!
lock = raf.getChannel().tryLock();
} catch (java.nio.channels.OverlappingFileLockException e) {
// NOTE: If this happens, 'lock' will be 'null' below anyway, so the error will still occur.
}
if (lock == null) {
// this means that someone else holds the lock already, which means that we abort
// NOTE: 'raf' will be closed in close()
throw new IOException("File is already being uploaded: Could not acquire exclusive lock on file");
}
}
/**
* Determines if the current user is allowed to modify the file in question.
* This modification is context-dependent, meaning that in this case, we examine if we are allowed to resume or delete the file as appropriate.
* @param file the file for which we want to examine the permission
* @return true if the user has the right to modify it, false otherwise.
*/
private boolean hasModifyPermission(File file) {
int permission;
if (fs.isOwner(user, file)) {
if (fs.getOffset() > 0 || append) {
permission = ActionPermission.RESUMEOWN;
} else {
permission = ActionPermission.OVERWRITEOWN;
}
} else {
if (fs.getOffset() > 0 || append) {
permission = ActionPermission.RESUME;
} else {
permission = ActionPermission.OVERWRITE;
}
}
return ServiceManager.getServices().getPermissions().hasPermission(permission, FileSystem.resolvePath(file), user);
}
public boolean isRunning() {
return transfer != null && transfer.isAlive();
}
public long getSpeed() {
if (transfer != null) {
return transfer.getCurrentSpeed();
} else {
return 0;
}
}
}