ch.cyberduck.core.ftp.FTPClient.java Source code

Java tutorial

Introduction

Here is the source code for ch.cyberduck.core.ftp.FTPClient.java

Source

package ch.cyberduck.core.ftp;

/*
 * Copyright (c) 2002-2010 David Kocher. All rights reserved.
 *
 * http://cyberduck.ch/
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * Bug fixes, suggestions and comments should be sent to:
 * dkocher@cyberduck.ch
 */

import ch.cyberduck.core.Protocol;
import ch.cyberduck.core.preferences.Preferences;
import ch.cyberduck.core.preferences.PreferencesFactory;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPCmd;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPSClient;
import org.apache.log4j.Logger;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

public class FTPClient extends FTPSClient {
    private static final Logger log = Logger.getLogger(FTPClient.class);

    private final SSLSocketFactory sslSocketFactory;

    private Protocol protocol;

    /**
     * Map of FEAT responses. If null, has not been initialised.
     */
    private Map<String, Set<String>> features;

    private final Preferences preferences = PreferencesFactory.get();

    public FTPClient(final Protocol protocol, final SSLSocketFactory f, final SSLContext c) {
        super(false, c);
        this.protocol = protocol;
        this.sslSocketFactory = f;
    }

    public void setProtocol(final Protocol protocol) {
        this.protocol = protocol;
    }

    @Override
    protected Socket _openDataConnection_(final String command, final String arg) throws IOException {
        final Socket socket = super._openDataConnection_(command, arg);
        if (null == socket) {
            throw new FTPException(this.getReplyCode(), this.getReplyString());
        }
        return socket;
    }

    @Override
    protected void _prepareDataSocket_(final Socket socket) throws IOException {
        if (preferences.getBoolean("ftp.tls.session.requirereuse")) {
            if (socket instanceof SSLSocket) {
                // Control socket is SSL
                final SSLSession session = ((SSLSocket) _socket_).getSession();
                if (session.isValid()) {
                    final SSLSessionContext context = session.getSessionContext();
                    context.setSessionCacheSize(preferences.getInteger("ftp.ssl.session.cache.size"));
                    try {
                        final Field sessionHostPortCache = context.getClass()
                                .getDeclaredField("sessionHostPortCache");
                        sessionHostPortCache.setAccessible(true);
                        final Object cache = sessionHostPortCache.get(context);
                        final Method method = cache.getClass().getDeclaredMethod("put", Object.class, Object.class);
                        method.setAccessible(true);
                        method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostName(),
                                String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session);
                        method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostAddress(),
                                String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session);
                    } catch (NoSuchFieldException e) {
                        // Not running in expected JRE
                        log.warn("No field sessionHostPortCache in SSLSessionContext", e);
                    } catch (Exception e) {
                        // Not running in expected JRE
                        log.warn(e.getMessage());
                    }
                } else {
                    log.warn(String.format("SSL session %s for socket %s is not rejoinable", session, socket));
                }
            }
        }
    }

    @Override
    protected void execAUTH() throws IOException {
        if (protocol.isSecure()) {
            if (FTPReply.SECURITY_DATA_EXCHANGE_COMPLETE != this.sendCommand("AUTH", this.getAuthValue())) {
                throw new FTPException(this.getReplyCode(), this.getReplyString());
            }
        }
    }

    @Override
    public void execPROT(final String prot) throws IOException {
        if (protocol.isSecure()) {
            if (FTPReply.COMMAND_OK != this.sendCommand("PROT", prot)) {
                throw new FTPException(this.getReplyCode(), this.getReplyString());
            }
            if ("P".equals(prot)) {
                // Private
                this.setSocketFactory(sslSocketFactory);
            }
        }
    }

    @Override
    public void execPBSZ(final long pbsz) throws IOException {
        if (protocol.isSecure()) {
            if (FTPReply.COMMAND_OK != this.sendCommand("PBSZ", String.valueOf(pbsz))) {
                throw new FTPException(this.getReplyCode(), this.getReplyString());
            }
        }
    }

    @Override
    protected void sslNegotiation() throws IOException {
        if (protocol.isSecure()) {
            final SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket(_socket_,
                    _socket_.getInetAddress().getHostAddress(), _socket_.getPort(), false);
            socket.setEnableSessionCreation(true);
            socket.setUseClientMode(true);
            socket.startHandshake();
            _socket_ = socket;
            _controlInput_ = new BufferedReader(
                    new InputStreamReader(socket.getInputStream(), getControlEncoding()));
            _controlOutput_ = new BufferedWriter(
                    new OutputStreamWriter(socket.getOutputStream(), getControlEncoding()));
        }
    }

    public List<String> list(final FTPCmd command) throws IOException {
        return this.list(command, null);
    }

    public List<String> list(final FTPCmd command, final String pathname) throws IOException {
        this.pret(command, pathname);

        Socket socket = _openDataConnection_(command, pathname);

        BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream(), getControlEncoding()));
        ArrayList<String> results = new ArrayList<String>();
        String line;
        while ((line = reader.readLine()) != null) {
            _commandSupport_.fireReplyReceived(-1, line);
            results.add(line);
        }

        reader.close();
        socket.close();

        if (!this.completePendingCommand()) {
            throw new FTPException(this.getReplyCode(), this.getReplyString());
        }
        return results;
    }

    /**
     * Query the server for a supported feature, and returns its values (if any).
     * Caches the parsed response to avoid resending the command repeatedly.
     *
     * @return if the feature is present, returns the feature values (empty array if none)
     * Returns {@code null} if the feature is not found or the command failed.
     * Check {@link #getReplyCode()} or {@link #getReplyString()} if so.
     * @throws IOException
     * @since 3.0
     */
    public String[] featureValues(String feature) throws IOException {
        if (!initFeatureMap()) {
            return null;
        }
        Set<String> entries = features.get(feature.toUpperCase(Locale.ROOT));
        if (entries != null) {
            return entries.toArray(new String[entries.size()]);
        }
        return null;
    }

    /**
     * Query the server for a supported feature, and returns the its value (if any).
     * Caches the parsed response to avoid resending the command repeatedly.
     *
     * @return if the feature is present, returns the feature value or the empty string
     * if the feature exists but has no value.
     * Returns {@code null} if the feature is not found or the command failed.
     * Check {@link #getReplyCode()} or {@link #getReplyString()} if so.
     * @throws IOException
     * @since 3.0
     */
    public String featureValue(String feature) throws IOException {
        String[] values = featureValues(feature);
        if (values != null) {
            return values[0];
        }
        return null;
    }

    /**
     * Query the server for a supported feature.
     * Caches the parsed response to avoid resending the command repeatedly.
     *
     * @param feature the name of the feature; it is converted to upper case.
     * @return {@code true} if the feature is present, {@code false} if the feature is not present
     * or the {@link #feat()} command failed. Check {@link #getReplyCode()} or {@link #getReplyString()}
     * if it is necessary to distinguish these cases.
     * @throws IOException
     * @since 3.0
     */
    public boolean hasFeature(String feature) throws IOException {
        if (!initFeatureMap()) {
            return false;
        }
        return features.containsKey(feature.toUpperCase(Locale.ROOT));
    }

    /**
     * Query the server for a supported feature with particular value,
     * for example "AUTH SSL" or "AUTH TLS".
     * Caches the parsed response to avoid resending the command repeatedly.
     *
     * @param feature the name of the feature; it is converted to upper case.
     * @param value   the value to find.
     * @return {@code true} if the feature is present, {@code false} if the feature is not present
     * or the {@link #feat()} command failed. Check {@link #getReplyCode()} or {@link #getReplyString()}
     * if it is necessary to distinguish these cases.
     * @throws IOException
     * @since 3.0
     */
    public boolean hasFeature(String feature, String value) throws IOException {
        if (!initFeatureMap()) {
            return false;
        }
        Set<String> entries = features.get(feature.toUpperCase(Locale.ROOT));
        if (entries != null) {
            return entries.contains(value);
        }
        return false;
    }

    /*
     * Create the feature map if not already created.
     */
    private boolean initFeatureMap() throws IOException {
        if (features == null) {
            // Don't create map here, because next line may throw exception
            final int reply = feat();
            if (FTPReply.NOT_LOGGED_IN == reply) {
                return false;
            } else {
                // we init the map here, so we don't keep trying if we know the command will fail
                features = new HashMap<String, Set<String>>();
            }
            boolean success = FTPReply.isPositiveCompletion(reply);
            if (!success) {
                return false;
            }
            for (String l : getReplyStrings()) {
                if (l.startsWith(" ")) { // it's a FEAT entry
                    String key;
                    String value = "";
                    int varsep = l.indexOf(' ', 1);
                    if (varsep > 0) {
                        key = l.substring(1, varsep);
                        value = l.substring(varsep + 1);
                    } else {
                        key = l.substring(1);
                    }
                    key = key.toUpperCase(Locale.ROOT);
                    Set<String> entries = features.get(key);
                    if (entries == null) {
                        entries = new HashSet<String>();
                        features.put(key, entries);
                    }
                    entries.add(value);
                }
            }
        }
        return true;
    }

    @Override
    public boolean retrieveFile(String remote, OutputStream local) throws IOException {
        this.pret(FTPCmd.RETR, remote);
        return super.retrieveFile(remote, local);
    }

    @Override
    public InputStream retrieveFileStream(String remote) throws IOException {
        this.pret(FTPCmd.RETR, remote);
        return super.retrieveFileStream(remote);
    }

    @Override
    public boolean storeFile(String remote, InputStream local) throws IOException {
        this.pret(FTPCmd.STOR, remote);
        return super.storeFile(remote, local);
    }

    @Override
    public OutputStream storeFileStream(String remote) throws IOException {
        this.pret(FTPCmd.STOR, remote);
        return super.storeFileStream(remote);
    }

    @Override
    public boolean appendFile(String remote, InputStream local) throws IOException {
        this.pret(FTPCmd.APPE, remote);
        return super.appendFile(remote, local);
    }

    @Override
    public OutputStream appendFileStream(String remote) throws IOException {
        this.pret(FTPCmd.APPE, remote);
        return super.appendFileStream(remote);
    }

    /**
     * http://drftpd.org/index.php/PRET_Specifications
     *
     * @param command Command to execute
     * @param file    Remote file
     * @throws IOException I/O failure
     */
    protected void pret(final FTPCmd command, final String file) throws IOException {
        if (this.hasFeature("PRET")) {
            if (!FTPReply.isPositiveCompletion(
                    this.sendCommand("PRET", String.format("%s %s", command.getCommand(), file)))) {
                throw new FTPException(this.getReplyCode(), this.getReplyString());
            }
        }
    }

    @Override
    public String getModificationTime(final String file) throws IOException {
        final String status = super.getModificationTime(file);
        if (null == status) {
            throw new FTPException(this.getReplyCode(), this.getReplyString());
        }
        return StringUtils.chomp(status.substring(3).trim());
    }
}