Package cu.ftpd.filesystem

Source Code of cu.ftpd.filesystem.FileSystem

/**
* 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.filesystem;

import cu.ftpd.FtpdSettings;
import cu.ftpd.ServiceManager;
import cu.ftpd.events.Event;
import cu.ftpd.filesystem.filters.ForbiddenFilesFilter;
import cu.ftpd.filesystem.metadata.Directory;
import cu.ftpd.filesystem.metadata.Metadata;
import cu.ftpd.filesystem.metadata.MetadataHandler;
import cu.ftpd.filesystem.permissions.ActionPermission;
import cu.ftpd.filesystem.permissions.PermissionDeniedException;
import cu.ftpd.logging.Logging;
import cu.ftpd.user.User;
import cu.ftpd.user.groups.NoSuchGroupException;
import cu.ftpd.user.userbases.Hex;
import cu.ftpd.user.userbases.NoSuchUserException;
import cu.settings.ConfigurationException;
import cu.settings.Settings;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.channels.FileChannel;
import java.security.AccessControlException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.zip.CRC32;

/**
* @author Markus Jevring
* @since 2007-05-07 - 21:38:57
* @version $Id: FileSystem.java 313 2011-10-30 12:37:39Z jevring $
*/
public class FileSystem {
    private static File root;
    private static String rcp;
    private static final Map<String, Section> sections = new HashMap<>();
    protected static Section defaultSection;

    private File pwdFile;
    private String pwd = "/";
    protected long offset = 0;
    protected final User user;
    protected Section section;
    protected File renameSource;
    protected String type = "ASCII"; // default
    protected String mode = null;
    // as much as I want to put milliseconds on the end, I can't because there are different standards. one is timezone, the other is increased precision
    // this used to be static, but since it is not thread safe, each instance has gotten their own copy
    public final DateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss", Locale.ENGLISH);
    protected final DateFormat time = new SimpleDateFormat("MMM d HH:mm", Locale.ENGLISH);
    protected final DateFormat year = new SimpleDateFormat("MMM d yyyy", Locale.ENGLISH);
    // 0=d|-, 1=owner, 2=group, 3=size, 4=date, 5=filename, 6=writable
    protected final MessageFormat listline = new MessageFormat("{0}r{6}xr{6}xr{6}x    1 {1}     {2} {3} {4} {5}");
    public static final ForbiddenFilesFilter forbiddenFiles = new ForbiddenFilesFilter();

    public FileSystem(User user) {
        this.user = user;
        section = defaultSection;
        pwdFile = root;
    }

    public static void initialize(FtpdSettings settings) throws IOException, ConfigurationException {
        loadSections(settings); // sets default section
        root = new File(defaultSection.getPath());
        // check that the directory pointed out in the default section actually exists (and is accessible)
        if (!root.exists() || !root.isDirectory()) {
            throw new FileNotFoundException("The root directory of the default section either does not exist or is not a directory.");
        } else {
            try {
                root.list();
            } catch (AccessControlException e) {
                throw new IOException("Permission to list root dir denied.", e);
            }
        }
        rcp = root.getCanonicalPath();
    }

    private static void loadSections(Settings settings) throws IOException, ConfigurationException {
        // default section:
        String name = "default";
        String path = settings.get("/filesystem/default/path");
        String owner = settings.get("/filesystem/default/owner");
        String group = settings.get("/filesystem/default/group");
        int ratio = settings.getInt("/filesystem/default/ratio");
        Section section = new Section(name, path, owner, group, ratio); // todo: the default path should be created if it doesn't already exist
        defaultSection = section;

        File sectionDir;
        int i = 1;
        while(true) {
            // loop over the sections in the file
            name = settings.get("/filesystem/sections/section[" + i + "]/name");
            if (name == null || "".equals(name)) {
                break;
            }
            path = settings.get("/filesystem/sections/section[" + i + "]/path");
            if (!path.startsWith("/")) {
                throw new IllegalArgumentException("Sections cannot have paths that do not start with '/'");
            }
            owner = settings.get("/filesystem/sections/section[" + i + "]/owner");
            if ("".equals(owner) || owner == null) { // these little if-statements handle the case when a setting is omitted and inherited from the default section
                owner = defaultSection.getOwner();
            }
            group = settings.get("/filesystem/sections/section[" + i + "]/group");
            if ("".equals(group) || group == null) {
                group = defaultSection.getGroup();
            }
            String ratioString = settings.get("/filesystem/sections/section[" + i + "]/ratio");
            if ("".equals(ratioString) || ratioString == null) {
                ratio = defaultSection.getRatio();
            } else {
                ratio = Integer.parseInt(ratioString); // this will work, since the xsd will validate that ratio is a number
            }
            i++;
            try {
                section = new Section(name, path, owner, group, ratio);
                sections.put(path, section);
            } catch (IllegalArgumentException e) {
                throw new ConfigurationException("Error in section: " + name, e);
            }

            // make sure that the sections have the proper owner and group in their parent directory listings.
            sectionDir = new File(defaultSection.getPath(), path);
            if (!sectionDir.exists()) {
                throw new FileNotFoundException("Section dir not found for section: " + name + ": " + sectionDir.getAbsolutePath());
            }
            ServiceManager.getServices().getMetadataHandler().setOwnership(sectionDir, owner, group);
            // _todo: see if we can't avoid triggering a write here. or what the hell, the write is so small, it doesn't matter!

            // make sure that /mp3 is listed as being owned by "mp3" etc.
            // how do we do this without using up extreme amounts of processing power per file?
            // we write a .metadata-file in the directories over the section directories when we start!!!!!!!!!!!!! (yes, that is the solution)
        }
    }

    /**
     * Accepts a path in one of the following forms:
     * - /path/to/some/dir
     * - /path/to/some/dir/
     * - /path/to/some/file.txt
     *
     * and extracts the name, i.e. "dir" in the first two cases and "file.txt" in the last.
     *
     * @param path the path from which the name is extracted.
     * @return the name if the file or directory, the last token.
     */
    public static String getNameFromPath(String path) {
        String name = path;
        if ("/".equals(path)) {
            return path;
        } else {
            if (name.endsWith("/")) {
                name = name.substring(0, name.length() - 1);
            }
            name = name.substring(name.lastIndexOf("/") + 1);
            return name;
        }
    }

    /**
     * Accepts a path in one of the following forms:
     * - /path/to/some/dir
     * - /path/to/some/dir/
     * - /path/to/some/file.txt
     *
     * and extracts the parent directory, i.e. "/path/to/some" in all of the cases above
     *
     * @param path the path from which the parent directory is extracted.
     * @return the parent directory.
     */
    public static String getParentDirectoryFromPath(String path) {
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
        path = path.substring(0, path.lastIndexOf("/"));
        return path;
    }

    /**
     * Resolves a <code>File</code> object to a path usable in the ftp. Also checks the legality of the file.
     * If the file cannot be resolved for some reason, it will return null.
     *
     * @param file the file to be translated. Can be either directory or file.
     * @return A string representation of the path for the ftp or null of resolution fails or the file is illegal.
     */
    public static String resolvePath(File file) {
        // maybe we should cache some responses here. It might not be needed though, since I don't think this particular part slows down execution that much
        // this isn't that big a hit. it won't affect performance that much, especially since the file operations are cached
        // but, if this turns out to be a big bottleneck later, we'll slap on a cache
        if (isLegalPath(file)) {
            String s;
            try {
                s = file.getCanonicalPath();
                s = s.substring(rcp.length());
                String t;
                if (s.isEmpty()) {
                    t = "/";
                } else {
                    t = s;
                }
                if (File.separatorChar != '/') { // this will happen on windows and older versions of mac
                    t = t.replace(File.separatorChar, '/');
                }
                return t;
            } catch (IOException e) {
                // There is a pretty good chance that something is terribly wrong if we end up here
                Logging.getErrorLog().reportCritical("We were unable to get the canonical path for " + file.getAbsolutePath());
                e.printStackTrace();
                // NOTE: returning null here is fine; it is in the specified behavior.
                return null;
            }
        } else {
            // NOTE: since we changed resolveFile() to return the root if the path was illegal, so must we resolve this to the root in the same case
            return "/";
        }
    }

    /**
     * Takes a specified path and resolves it against the current directory. Any path beginning with '/'
     * will be resolved against the root.<br>
     * If the specified path resolved to be outside the ftp root, the ftp root itself is returned.<br>
     * NOTE: The path must be an ftp-path, and NOT a system path
     *
     * @param path the FTP path to be resolved.
     * @return A file indicated by the path as specified above.
     */
    public File resolveFile(String path) {
        /*
        any resolved file should start with the root path (e.g. c:\ftproot\)
        thus, any RESOLVED path that is equal to or shorter than that (i.e. c:\tmp\ or /root/) should resolve to "/"
        therefor, we resolve first, then we check the canonical path to see if that is below the root.
        if it is below the root, we return it, otherwise we return the root
        that way we don't have to throw any exceptions
         */
        File file;
        if (path.startsWith("/")) {
            file = new File(root, path);
        } else {
            file = new File(pwdFile, path);
        }
        if (isLegalPath(file)) {
            return file;
        } else {
            // this means that the file resolved to somewhere outside the root, so we return the root.
            // this way it is impossible to get outside the root, but we don't have to throw any exceptions.
            // this works much like glob()
            return root;
        }
    }

    /**
     * Takes an ftpd path and returns the 'real' path of that path.
     * I.e. in the default case, passing "/tmp" to this function would return "/cuftpd/site/tmp".
     *
     * @param ftppath the ftpd path we want resolved
     * @return the "real" path that the provided ftpd path represents, resolved as decribed above.
     */
    public String resolveRealPath(String ftppath) {
        // While this may seem simplistic and useless, it is very useful for the integration with cubnc, and getting away from having things outside the FileSystem know about File-type objects
        return resolveFile(ftppath).getAbsolutePath();
    }

    /**
     * Takes an ftp path, absolute or relative, and resolves it to the absolute ftp path.
     *
     * @param ftppath the path to be resolved.
     * @return the absolute ftp path of the path specified.
     */
    public String resolveFtpPath(String ftppath) {
        // While this may seem simplistic and useless, it is very useful for the integration with cubnc, and getting away from having things outside the FileSystem know about File-type objects
        return resolvePath(resolveFile(ftppath));
    }

    /**
     * Checks whether a file is legal or not. A legal file is a file that is anywhere within the chroot of the ftpd.
     * @param file the file to be checked.
     * @return true if it lies within the path, false otherwise.
     */
    private static boolean isLegalPath(File file) {
        // this checks if the supplied directory is reachable (and not above/outside the specified root)
        if (file != null) {
            try {
                String s = file.getCanonicalPath();
                return s.startsWith(rcp);
            } catch (IOException e) {
                // There is a pretty good reason that something is terribly wrong if we end up here
                Logging.getErrorLog().reportCritical("We were unable to get the canonical path for " + file.getAbsolutePath());
                e.printStackTrace();
                return false;
            }
        } else {
            // file can be null, if we did .getParentFile() on something like "/" or "c:\"
            return false;
        }
    }
    private boolean isEmpty(File directory) {
        // gotta love windows dir request caching
        // NOTE we can't just do directory.isEmpty() here, since it is still considered empty, even if the forbidden files are present
        return (directory.list(forbiddenFiles).length == 0);
    }
    public boolean isOwner(User user, File file) {
        return ServiceManager.getServices().getMetadataHandler().isOwner(user.getUsername(), file);
    }

    public List<String> nlst(String path) throws PermissionDeniedException, AccessControlException, FileNotFoundException {
        // NOTE: we are not interested in user/group metadata here, since we are just returning the names of the files.
        File dir;
        if (path != null && !"".equals(path)) {
            dir = resolveFile(path);
        } else {
            // nlst the pwd
            dir = pwdFile;
        }
        if (!ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.LIST, resolvePath(dir), user)) {
            throw new PermissionDeniedException(ActionPermission.LIST, resolvePath(dir), user);
        }
        if (dir.exists() && dir.isDirectory()) {
            File[] files = dir.listFiles(forbiddenFiles);
            List<String> listOfFiles = new LinkedList<>();
            if (files != null && pwdFile.exists()) {
                for (File file : files) {
                    if (!ServiceManager.getServices().getPermissions().isVisible(user, file.getName(), pwd, file.isDirectory())) {
                        // this file or directory was hidden from this user, so we don't display it
                        continue;
                    }
                    listOfFiles.add(file.getName());
                }
            }
            return listOfFiles;
        } else {
            // This is so that nlst to unknown paths does not list the root, but rather gives an error
            throw new FileNotFoundException("The specified directory does not exist.");
        }
    }

    /**
     * Returns an ftp-compatible listing the specified directory.
     * @param path a directory.
     * @return a list containing lines that represent the file entities in this directory,
     * formatted as a "ls -la" output.
     * @throws java.security.AccessControlException if the policy files forbid access to this directory.
     * @throws java.io.FileNotFoundException if the specified directory could not be found.
     * @throws PermissionDeniedException if the permission system denies us entry to this directory.
     */
    public List<String> list(String path) throws AccessControlException, FileNotFoundException, PermissionDeniedException {
        String pwd = this.pwd;
        File pwdFile = this.pwdFile;
        if (path != null) {
            pwdFile = resolveFile(path);
            pwd = resolvePath(pwdFile);
            //pwd = (path.startsWith("/")) ? path : pwd + path; // from diff
        }
        if (!ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.LIST, pwd, user)) {
            throw new PermissionDeniedException(ActionPermission.LIST, pwd, user);
        }
        // note: 'pwd' and 'pwdFile' are now being shadowed by local variables, as LIST is actually supposed to support parameters
        //long start = System.currentTimeMillis();
        File[] files = pwdFile.listFiles(forbiddenFiles); // only list allowed files
        LinkedList<String> lines;
        if (files == null || !pwdFile.exists()) {
            // it the dir is empty we get a list of length 0, so it'll still be executed properly
            throw new FileNotFoundException("Directory not found: " + pwd);
        } else { // it'll be null if pwdFile did not indicate a directory, if it didn't exist, or if there was some I/O Error
            int currentYear = Calendar.getInstance().get(Calendar.YEAR);
            lines = new LinkedList<>();
            Calendar c = Calendar.getInstance();
            String dateline;
            Directory metadataDirectory = ServiceManager.getServices().getMetadataHandler().getDirectory(pwdFile);
            String group;
            String owner;
            for (File file : files) {
                if (!ServiceManager.getServices().getPermissions().isVisible(user, file.getName(), pwd, file.isDirectory())) {
                    // this file or directory was hidden from this user, so we don't display it
                    continue;
                }
                c.setTimeInMillis(file.lastModified());
                if (c.get(Calendar.YEAR) == currentYear) {
                    // do the listing with time instead of year
                    dateline = time.format(c.getTime());
                } else {
                    // do the listing with year instead of time
                    dateline = year.format(c.getTime());
                }
                // default: (actually, section can never be null, since we always have a default section)
                owner = "cuftpd";
                group = "cuftpd";

                // next highest default is the parent group metadata
                if (section != null) {
                    owner = section.getOwner();
                    group = section.getGroup();
                }

                // next highest default is to check if there is any metadata information registered to the entity.
                if (metadataDirectory != null) {
                    Metadata metadata = metadataDirectory.getMetadata(file.getName());
                    if (metadata != null) { // this shouldn't return some default object, because if it does, it overrides the section owner/group
                        owner = metadata.getUsername();
                        // we can have users without groups, handle this
                        if (!"".equals(metadata.getGroupname()) && metadata.getGroupname() != null) {
                            group = metadata.getGroupname();
                        }
                    } else {
                        //System.out.println("Metadata for " + file.getAbsolutePath() + " was null");
                    }
                } else {
                    //System.out.println("Directory for " + pwdFile.getAbsolutePath() + " was null!");
                }

                // note: if we don't do ""+file.length() here, the formatter will insert spaces or commas or other stuff (depending on the locale) to format the number.
                // NOTE: the formatter is EXPENSIVE, but still much simpler than just concatenating, or using a string buffer
                lines.add(listline.format(new String[]{(file.isDirectory() ? "d" : "-"), owner, group, String.valueOf(file.length()), dateline, file.getName(), (file.canWrite() ? "w" : "-")}));

            }
            //System.out.println("list took " + (System.currentTimeMillis() - start) + " milliseconds");
        }
        return lines;
    }
    public List<String> mlsd(String path) throws FileNotFoundException, PermissionDeniedException {
        File dir = resolveFile(path);
        if (!ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.LIST, resolvePath(dir), user)) {
            throw new PermissionDeniedException(ActionPermission.LIST, resolvePath(dir), user);
        }
        File[] files = dir.listFiles(forbiddenFiles); // only list allowed files
        LinkedList<String> lines;
        if (files == null || !dir.exists()) {
            // it the dir is empty we get a list of length 0, so it'll still be executed properly
            throw new FileNotFoundException("Directory not found: " + pwd);
        } else { // it'll be null if pwdFile did not indicate a directory, if it didn't exist, or if there was some I/O Error
            lines = new LinkedList<>();
            String type = "unknown";
            Section section = getSection(dir);
            Directory metadataDirectory = ServiceManager.getServices().getMetadataHandler().getDirectory(dir);

            // cdir (absolute)
            lines.add(mlstResolved(dir, true, section, "cdir", metadataDirectory));
            try {
                // pdir (absolute)
                File parent = dir.getParentFile();
                if (parent != null) {
                    lines.add(mlstResolved(dir.getParentFile(), true, section, "pdir", metadataDirectory));
                } else {
                    // using this as a parent dir
                    lines.add(mlstResolved(dir, true, section, "pdir", metadataDirectory));
                }
            } catch (AccessControlException e) {
                // this means that the parent dir was off limits, like it is if we are in "/", so just do not include a parent dir
            }

            for (File file : files) {
                if (!ServiceManager.getServices().getPermissions().isVisible(user, file.getName(), pwd, file.isDirectory())) {
                    // this file or directory was hidden from this user, so we don't display it
                    continue;
                }
                if (file.isFile()) {
                    type = "file";
                } else if (file.isDirectory()) {
                    type = "dir";
                }
                // use resolved here, since we already know that the file is ok (and resolved)
                lines.add(mlstResolved(file, false, section, type, metadataDirectory));
            }
            //System.out.println("list took " + (System.currentTimeMillis() - start) + " milliseconds");
        }
        return lines;
    }

    public String mlst(String filename) throws FileNotFoundException {
        // returns information about the file referenced in "filename"
        File file = resolveFile(filename);
        String type = "unknown";
        if (file.exists()) {
            if (file.isFile()) {
                type = "file";
            } else if (file.isDirectory()) {
                type = "dir";
            }
            Directory metadataDirectory;
            try {
                metadataDirectory  = ServiceManager.getServices().getMetadataHandler().getDirectory(file.getParentFile());
            } catch (Exception e) {
                // what the hell are we capturing here?!
                // Probably the illegal argument that the directory is a file, but why?
                // well, it seems to be working, but more testing is needed.
                // see folks, this is why we leave comments all over the place!
                metadataDirectory  = ServiceManager.getServices().getMetadataHandler().getDirectory(file);
            }
            return mlstResolved(file, true, getSection(file), type, metadataDirectory);
        } else {
            throw new FileNotFoundException(resolvePath(file));
        }
    }

    private String mlstResolved(File file, boolean displayAbsolutePathname, Section section, String type, Directory metadataDirectory) {
        // default: (actually, section can never be null, since we always have a default section)
        String owner = "cuftpd";
        String group = "cuftpd";

        if (section != null) {
            owner = section.getOwner();
            group = section.getGroup();
        }

        // next highest default is to check if there is any metadata information registered to the entity.
        if (metadataDirectory != null) {
            Metadata metadata = metadataDirectory.getMetadata(file.getName());
            if (metadata != null) { // this shouldn't return some default object, because if it does, it overrides the section owner/group
                owner = metadata.getUsername();
                // we can have users without groups, handle this
                if (!"".equals(metadata.getGroupname()) && metadata.getGroupname() != null) {
                    group = metadata.getGroupname();
                }
            } else {
                //System.out.println("Metadata for " + file.getAbsolutePath() + " was null");
            }
        } else {
            //System.out.println("Directory for " + pwdFile.getAbsolutePath() + " was null!");
        }
        StringBuilder sb = new StringBuilder(250);
        sb.append("Type=").append(type).append(";");
        sb.append("Size=").append(file.length()).append(";");
        sb.append("Modify=").append(DATETIME_FORMAT.format(new Date(file.lastModified()))).append(";");
        sb.append("UNIX.owner=").append(owner).append(";");
        sb.append("UNIX.group=").append(group).append(";");
        if (displayAbsolutePathname) {
            sb.append(" ").append(resolvePath(file)); // always have the space before the filename
        } else {
            sb.append(" ").append(file.getName());
        }
        return sb.toString();
    }

    public String getFtpParentWorkingDirectory() {
        return pwd;
    }

    /**
     * Returns the string representation of the absolute path of the file representing the current directory
     * @return
     */
    public String getRealParentWorkingDirectoryPath() {
        return pwdFile.getAbsolutePath();
    }

  public File getPwdFile() {
    return pwdFile;
  }

  public String getDotMessageForCurrentDir() {
        try {
            return readTextFile(resolveFile(".message"));
        } catch (Exception e) {
            return null;
        }
    }
    public void shutdown() {
        // this doesn't do anything, but it's used in cubnc
    }

    public void cwd(String directory) throws PermissionDeniedException, AccessControlException, IOException {
        // todo: possible feature: make it possible to chroot users/groups in different dirs
        if (".".equals(directory)) {
            // this is just used as anti-idle
        } else {
            File t;
            if ("..".equals(directory)) {
                t = pwdFile.getParentFile();
            } else {
                t = resolveFile(directory);
                /*
                if (directory.startsWith("/")) {
                    t = new File(root, directory);
                } else {
                    t = new File(pwdFile, directory);
                }
                */
            }
            if (t == null) {
                throw new FileNotFoundException("No such directory.");
            } else if (!t.exists()) {
                throw new FileNotFoundException("No such directory: " + directory);
            } else if (!t.isDirectory()) {
                throw new IOException("Not a directory: " + directory);
            } else if (!t.canExecute()) { // added t.canExecute() to check for +x on the dir. if that's not there, we can't enter the dir
                throw new IOException("Cannot enter directory: " + directory);
            } else if (!isLegalPath(t)) {
                throw new FileNotFoundException("No such directory: " + directory);
                // NOTE:
                // there was a bug here that allowed access to directories outside the ftproot IF the user had allowed it in their policy files.
                // the instructions clearly state, and the defaults indicate, that the highest allowed path should be the same as the ftproot
                // this makes it unlikely that there are sites that are vulnerable, but it is bad none the less.
                // the bug could only by found by using more lax permission settings
                // the problem here is that, since resolvePath never returns null, the path looks right, when in actuality, it is wrong
            } else {
                String path = resolvePath(t);
                if (!ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.CWD, path, user)) {
                    throw new PermissionDeniedException(ActionPermission.CWD, path, user);
                }
                pwdFile = t;
                if (path.length() == 0) {
                    pwd = "/";
                } else {
                    pwd = path;
                }
                section = getSection(pwd); // this must take in an absolute path rooted in the ftpd root
            }
        }
    }
    public String mkd(String directory) throws PermissionDeniedException, AccessControlException, IOException {
        File mkd = resolveFile(directory);
        String ftpPathToDir = resolvePath(mkd);
        // we want to check the permissions in the directory that contains the directory we are aiming for, and since
        // MKD can take parameters with full path names, we need to check in the parent
        if (ftpPathToDir == null || !ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.MKDIR, ftpPathToDir, user)) {
            throw new PermissionDeniedException(ActionPermission.MKDIR, ftpPathToDir, user);
        }
        if (!mkd.exists()) {
            boolean success = mkd.mkdirs();
            if (success) {
              MetadataHandler metadataHandler = ServiceManager.getServices().getMetadataHandler();
              metadataHandler.setOwnership(mkd, user.getUsername(), user.getPrimaryGroup());
              File f = mkd.getParentFile();
              while (f != null && !metadataHandler.hasOwner(f) && isLegalPath(f.getParentFile())) {
                metadataHandler.setOwnership(f, user.getUsername(), user.getPrimaryGroup());
                f = f.getParentFile();
              }
                return resolvePath(mkd);
            } else {
                throw new IOException("Could not create directory.");
            }
        } else {
            throw new IOException("Could not create directory: directory exists.");
        }
    }
   
    public void rmd(String path) throws PermissionDeniedException, AccessControlException, IOException {
        File directory = resolveFile(path);
        String ftpPathToDir = resolvePath(directory);
        // we want to check the permissions in the directory that contains the directory we are aiming for, and since RMD can take parameters with full path names, we need to check in the parent
        if (ftpPathToDir == null || !ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.RMDIR, ftpPathToDir, user)) {
            throw new PermissionDeniedException(ActionPermission.RMDIR, ftpPathToDir, user);
        }
        if (directory.exists()) {
            if (directory.isDirectory()) {
                if (isEmpty(directory)) {
                    // apparently we need to delete the hidden files first, since rmd.delete() will not succeed if there are files left in the directory.
                    File f = new File(directory, ".raceinfo");
                    f.delete();
                    f = new File(directory, ".metadata");
                    f.delete();
                    if (!directory.delete()) {
                        throw new IOException("Failed to remove directory.");
                    }
                    ServiceManager.getServices().getMetadataHandler().delete(directory);
                } else {
                    throw new IOException("Failed to remove directory: directory was not empty.");
                }
            } else {
                throw new IOException("Failed to remove directory: path specified is not a directory.");
            }
        } else {
            throw new FileNotFoundException("No such directory: " + path);
        }
    }

    /**
     * Removes the directory or name specified by name without removing the credits awarded for uploading the directory.
     *
     * @param path the file to be wiped.
     * @return true if the wipe was successful, false otherwise.
     * @throws java.io.FileNotFoundException if the file corresponding to the path did not exist.
     */
    public boolean wipe(String path) throws FileNotFoundException {
        // todo: support dynamically removing a "-r" if it was supplied as an argument before the pathname.
        File file = resolveFile(path);
        boolean wasDir = file.isDirectory();
        long length = file.length();
        // and then if the delete fails, we can throw the exception, and if not, we keep the exception quiet, since we deleted a symlink
        boolean fileMissing = false;
        if (!file.exists()) {
            fileMissing = true;
        }
        // do a recursive delete, since we can't delete directories if they are not empty.
        boolean deleted = recursiveDelete(file);
        if (deleted) {
            ServiceManager.getServices().getMetadataHandler().delete(file); // this doesn't rely on the existence of the file, so it still works

            Event event;
            if (wasDir) {
                event = new Event(Event.REMOVE_DIRECTORY, user, getRealParentWorkingDirectoryPath(), getFtpParentWorkingDirectory());
                event.setProperty("dirlog.deleteRecursively", "true");
            } else {
                event = new Event(Event.DELETE_FILE, user, getRealParentWorkingDirectoryPath(), getFtpParentWorkingDirectory());
                event.setProperty("file.size", String.valueOf(length));
            }
            event.setProperty("file.path.real", resolveRealPath(path));
            event.setProperty("file.path.ftp", resolveFtpPath(path));
            event.setProperty("site.section", getSection(path).getName());
            ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
        } else {
            if (fileMissing) {
                // we do this here, so that we can delete orphaned symlinks, even though they report as being non-existent.
                throw new FileNotFoundException("File not deleted: file not found: " + resolvePath(file));
            } // else it is some unknown error
            if (wasDir) {
                file.setWritable(true); // make sure the directory is writable (eg: nuked release)
            }
        }
        return deleted;
    }

    /**
     * Deletes all files and folders in the specified dir, recursively.
     *
     * @param dir the dir in which to perform the recursive deletes.
     * @return true if the recursive delete was succesful, false otherwise.
     */
    private boolean recursiveDelete(File dir) {
        if (dir.isDirectory()) {
            for (File f : dir.listFiles()) {
                boolean ok = recursiveDelete(f);
                if (!ok) {
                    return false;
                }
            }
        }
        return dir.delete();
    }

    public void delete(String path) throws PermissionDeniedException, AccessControlException, IOException {
        File file = resolveFile(path);
        String ftpPathToFile = resolvePath(file);
        // check if we are the owner
        // if we are the owner, check if we have deleteown permissions in this dir
        // if we don't have it, throw a permission exception
        // if we have it, delete
        // if we are not the owner, check if we have delete permissions in this dir
        // if we don't have it, throw a permission exception
        // if we have it, delete
        int permission;
        if (isOwner(user, file)) {
            permission = ActionPermission.DELETEOWN;
        } else {
            permission = ActionPermission.DELETE;
        }
        if (!ServiceManager.getServices().getPermissions().hasPermission(permission, ftpPathToFile, user)) {
            throw new PermissionDeniedException(permission, ftpPathToFile, user);
        }
        if (!file.exists()) {
            throw new FileNotFoundException("File not deleted: file not found.");
        } else if (file.isDirectory()) {
            throw new IOException("File not deleted: filename indicates a directory");
        }
        long filesize = file.length();
        if (!file.delete()) {
            throw new IOException("File not deleted: unknown error.");
        }
        // resolve the pathname so that we can extract the real section, since we can accept commands that have path info in them
        if (!user.hasLeech()) {
            Section section = getSection(file);
            int ratio = section.getRatio();
            user.takeCredits(filesize * ratio);
        }

        // _todo: verify that we actually have a file, so that it isn't null (and remove the check inside)
        // this is verified inside FileSystem, and an exception thrown if not
        ServiceManager.getServices().getMetadataHandler().delete(file);
    }
    public void rest(long offset) {
        this.offset = offset;
    }
    public long getOffset() {
        return offset;
    }

    private String readTextFile(File file) throws AccessControlException, IOException {
        StringBuilder sb = new StringBuilder(250);

        BufferedReader in = null;
        try {
            in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "ISO-8859-1"));
            String line;
            while ((line = in.readLine()) != null) {
                sb.append(line).append("\r\n");
            }
        } finally {
            if (in != null) {
                in.close();
            }
        }
        return sb.toString();
    }

    /**
     * This allows reading files outside the ftp root. Use a policy file to restrict access to the directory structure.
     * @param path the (full) path of the file. Relative paths are relative from the running dir.
     * @return the contens of the file as a string with newline characters.
     * @throws java.security.AccessControlException throws this exception if reading from that specified file is not permitted.
     * @throws java.io.IOException thrown if the underlying I/O system causes an error.
     */
    public String readExternalTextFile(String path) throws AccessControlException, IOException {
        return readTextFile(new File(path));
    }

    public Section getCurrentSection() {
        return section;
    }

    /**
     * Returns the section with the specified name.
     * If no section matched, the default section is returned.
     *
     * @param name the name of the section in question.
     * @return the section with the name matching. If none is found, a default section is returned.
     * @author [h0D] <d0h@linux.nu>
     */
    public Section getSectionByName(String name) {
        Section section = defaultSection;
        if (name == null) {
          return section;
        }
        for (Map.Entry<String, Section> entry : sections.entrySet()) {
            if (name.equals(entry.getValue().getName())) {
              return entry.getValue();
            }
        }
        return section; // since we have the default settings for owner and group outside the actual default section
    }

    private void checkRenamePermissions(File file) throws PermissionDeniedException {
        if (isOwner(user, file)) {
            // check permission RENAMEOWN
            if (!ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.RENAMEOWN, resolvePath(file), user)) {
                throw new PermissionDeniedException(ActionPermission.RENAMEOWN, resolvePath(file), user);
            }
        } else {
            // check permission RENAME
            if (!ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.RENAME, resolvePath(file), user)) {
                throw new PermissionDeniedException(ActionPermission.RENAME, resolvePath(file), user);
            }
        }
    }

    public String getRealRenameSource() {
        return renameSource.getAbsolutePath();
    }

    public String getFtpRenameSource() {
        return resolvePath(renameSource);
    }

    public void rnfr(String source) throws PermissionDeniedException, AccessControlException, IOException {
        File t = resolveFile(source);
        if (!t.exists()) {
            throw new FileNotFoundException(FileSystem.resolvePath(t));
        }
        checkRenamePermissions(t);
        renameSource = resolveFile(source);
    }

    public boolean rnto(String target) throws PermissionDeniedException, AccessControlException, IOException {
        if (renameSource != null) {
            File renameTarget = resolveFile(target);
            if ((renameSource.isDirectory() && renameTarget.isFile()) || (renameTarget.exists())) {
                // you can't rename/move a directory to an existing file, throw an exception
                throw new IllegalArgumentException("Can't rename/move a directory to a file, or the target already exists");
            }
            checkRenamePermissions(renameTarget);
            boolean success = renameSource.renameTo(renameTarget);
            if (success) {
                // Note: we can leave the MetadataHandler stuff here, since that's used all over this class anyway
                // and we pretty much have to leave the dirlog stuff in here, since we need both source and target.
                ServiceManager.getServices().getMetadataHandler().move(renameSource, renameTarget, user.getUsername(), user.getPrimaryGroup());
            }
            return success;
        } else {
            throw new IOException("No rename source specified, please issue RNFR first");
        }
    }

    /**
     * Creates a copy of the specified file as "filename.bak" in the same directory.
     * @param file the file to be copied.
     */
    public static void fastBackup(File file) {
        FileChannel in = null;
        FileChannel out = null;
        FileInputStream fin = null;
        FileOutputStream fout = null;
        try {
            in = (fin = new FileInputStream(file)).getChannel();
            out = (fout = new FileOutputStream(file.getAbsolutePath() + ".bak")).getChannel();
            // make a backup copy of the file:
            in.transferTo(0, in.size(), out);
        } catch (IOException e) {
            Logging.getErrorLog().reportError("Fast backup failure (" + file.getAbsolutePath() + "): " + e.getMessage());
        } finally {
            if (fin != null) {
                try {
                    fin.close();
                } catch (IOException e) {
                    Logging.getErrorLog().reportException("Failed to close file input stream", e);
                    //e.printStackTrace();
                }
            }
            if (fout != null) {
                try {
                    fout.close();
                } catch (IOException e) {
                    Logging.getErrorLog().reportException("Failed to close file output stream", e);
                    //e.printStackTrace();
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    Logging.getErrorLog().reportException("Failed to close file channel", e);
                    //e.printStackTrace();
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    Logging.getErrorLog().reportException("Failed to close file channel", e);
                    //e.printStackTrace();
                }
            }
        }
    }

    /**
     * Finds a unique filename and returns the corresponding File object.
     * @return a unique file.
     * @param prefix a prefix to the filename to make it mroe difficult to get collisions.
     */
    public File createUniqueFile(String prefix) {
        File f = new File(pwdFile, prefix + Integer.toString(new Random().nextInt() & 0xffff));
        while (f.exists()) {
            System.err.println("we tried to create a unique file that already existed, what are the odds!");
            f = new File(pwdFile, prefix + Integer.toString(new Random().nextInt() & 0xffff));
        }
        // the odds of this hitting existing files indefinitely are virtually none
        return f;
    }

    public Section getDefaultSection() {
        return defaultSection;
    }

    /**
     * Returns the most qualified (longest path name) section for the specified path.
     * If the ftp root is in "/cubnc/site", and the ftp directory we are accessing is "/mp3" (which is physically situated in "/cubnc/site/mp3/", the string that must be passed to this method is "/mp3"
     *
     * @param path an absolute path, with its root in the ftp-root.
     * @return the best matched section. If none is found, a default section is returned.
     */
    public Section getSection(String path) {
        int sizeOfMatch = 0;
        Section section = defaultSection;
        for (Map.Entry<String, Section> entry : sections.entrySet()) {
            //System.out.println("checking to see if '" + path + "' start with '" + entry.getKey() + "' which it " + (path.startsWith(entry.getKey()) ? "DOES" : "doesn't"));
            if (path.startsWith(entry.getKey())) {
                // this is a matching section, but is it the best matching section?
                if (entry.getKey().length() > sizeOfMatch) {
                    // this is a better match than any previous ones
                    sizeOfMatch = entry.getKey().length();
                    section = entry.getValue();
                }
            }
        }
        //System.out.println("found section: " + section.getName());
        return section; // since we have the default settings for owner and group outside the actual default section
    }

    /**
     * Returns the section that the file indicated belongs to.<br>
     * It does this by resolving the file to a path and then invokes #getSection(String)
     * @param file the file in question
     * @return the section the file belongs to
     */
    public Section getSection(File file) {
        return getSection(resolvePath(file));
    }

    /**
     * Returns the length of the file specified byt the ftp path.
     *
     * @param file FTP path of the file we want to know the length of.
     * @return the file length
     * @throws java.io.FileNotFoundException if the file was not found
     */
    public long length(String file) throws FileNotFoundException {
        File f = resolveFile(file);
        if (!f.exists()) {
            throw new FileNotFoundException(resolvePath(f));
        }
        return f.length();
    }

    public long lastModified(String file) throws FileNotFoundException {
        File f = resolveFile(file);
        if (!f.exists()) {
            throw new FileNotFoundException(resolvePath(f));
        }
        return f.lastModified();
    }

    public String xmd5(String filename, long start, long end) throws NoSuchAlgorithmException, IOException {
        MessageDigest digest = MessageDigest.getInstance("MD5");
        return digest(filename, digest, start, end);
    }

    public String xsha1(String filename, long start, long end) throws NoSuchAlgorithmException, IOException {
        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        return digest(filename, digest, start, end);
    }

    private String digest(String filename, MessageDigest digest, long start, long end) throws IOException {
        File file = resolveFile(filename);
        if (!file.exists() && filename.startsWith("\"") && filename.endsWith("\"")) {
            // strip the damn quotes and get the file inside them
            // ftprush sends quotes all the time, I don't know how the other clients do it.
            // quotes are good, in a sense, but it would be nice if the damn standard mentioned them
            file = resolveFile(filename.substring(1, filename.length() - 1));
        }
        FileInputStream in = null;
        try {
            byte[] buf = new byte[8192];
            int len;
            if (start == 0 && end == 0) {
                // simple, fast, lovely
                in = new FileInputStream(file);
            } else {
                // complex, ugly, disgusting
                in = new LimitedFileInputStream(file, start, end);
            }
            while ((len = in.read(buf, 0, buf.length)) != -1) {
                digest.update(buf, 0, len);
            }
            return new String(Hex.bytesToHex(digest.digest()));
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    Logging.getErrorLog().reportException("Failed to close file input stream", e);
                    //e.printStackTrace();
                }
            }
        }
    }

    public String xcrc(String filename, long start, long end) throws IOException {
        File file = resolveFile(filename);
        if (!file.exists() && filename.startsWith("\"") && filename.endsWith("\"")) {
            // _todo: move this stripping of quotes out to the Connection (PI)
            // no, it's good to have it here, since we can check .exists()
            // strip the damn quotes and get the file inside them
            // ftprush sends quotes all the time, I don't know how the other clients do it.
            // quotes are good, in a sense, but it would be nice if the damn standard mentioned them
            file = resolveFile(filename.substring(1, filename.length() - 1));
        }
        CRC32 crc = new CRC32();
        FileInputStream in = null;
        try {
            byte[] buf = new byte[8192];
            int len;
            if (start == 0 && end == 0) {
                // simple, fast, lovely
                in = new FileInputStream(file);
            } else {
                // complex, ugly, disgusting
                in = new LimitedFileInputStream(file, start, end);               
            }
            while ((len = in.read(buf, 0, buf.length)) != -1) {
                crc.update(buf, 0, len);
            }
            return Long.toHexString(crc.getValue()).toUpperCase();
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    public void setLastModified(String filename, long time) throws PermissionDeniedException, FileNotFoundException {
        File file = resolveFile(filename);
        if (file.exists()) {
            if (!ServiceManager.getServices().getPermissions().hasPermission(ActionPermission.SETTIME, resolvePath(file), user)) {
                throw new PermissionDeniedException(ActionPermission.SETTIME, resolvePath(file), user);
            }
            file.setLastModified(time);
        } else {
            throw new FileNotFoundException(resolvePath(file));
        }
    }

    public void chown(String path, String username, String groupname, boolean recursive) throws NoSuchGroupException, NoSuchUserException {
        // do this to see if the user and/or group exists before doing anything
        if (username != null) {
            ServiceManager.getServices().getUserbase().getUser(username);
        }
        if (groupname != null) {
            ServiceManager.getServices().getUserbase().getGroup(groupname);
        }
        File file = resolveFile(path);
        ServiceManager.getServices().getMetadataHandler().setOwnership(file, username, groupname, recursive);
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getMode() {
        return mode;
    }

    public void setMode(String mode) {
        this.mode = mode;
    }

    public long getFreeSpaceInParentWorkingDirectory() {
        return pwdFile.getFreeSpace();
    }

    public long getTotalSpaceInParentWorkingDirectory() {
        return pwdFile.getTotalSpace();
    }

    /**
     * Accepts a real filesystem path and determines whether or not this is a directory.
     * @param realPath The path to be checked.
     * @return true if the path represents a directory, false otherwise
     */
    public boolean isDirectory(String realPath) {
        File f = new File(realPath);
        return f.isDirectory();
    }
}
TOP

Related Classes of cu.ftpd.filesystem.FileSystem

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.