Package cu.ftpd.commands.transfer

Source Code of cu.ftpd.commands.transfer.CommandSTOR

/**
* 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;
        }
    }
}
TOP

Related Classes of cu.ftpd.commands.transfer.CommandSTOR

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.