ch.cyberduck.core.Session.java Source code

Java tutorial

Introduction

Here is the source code for ch.cyberduck.core.Session.java

Source

package ch.cyberduck.core;

/*
 *  Copyright (c) 2005 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.analytics.AnalyticsProvider;
import ch.cyberduck.core.analytics.QloudstatAnalyticsProvider;
import ch.cyberduck.core.cdn.Distribution;
import ch.cyberduck.core.cdn.DistributionConfiguration;
import ch.cyberduck.core.cloudfront.CloudFrontDistributionConfiguration;
import ch.cyberduck.core.i18n.Locale;
import ch.cyberduck.core.identity.DefaultCDNCredentialsIdentityConfiguration;
import ch.cyberduck.core.identity.IdentityConfiguration;
import ch.cyberduck.core.threading.BackgroundException;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @version $Id: Session.java 10902 2013-04-22 09:29:42Z dkocher $
 */
public abstract class Session implements TranscriptListener {
    private static final Logger log = Logger.getLogger(Session.class);

    /**
     * Encapsulating all the information of the remote host
     */
    protected Host host;

    /**
     * Caching files listings of previously listed directories
     */
    private Cache cache = new Cache() {
        @Override
        public String toString() {
            return String.format("Cache for %s", Session.this.toString());
        }
    };

    private UseragentProvider ua;

    private boolean unsecurewarning = Preferences.instance().getBoolean("connection.unsecure.warning");

    private Set<ConnectionListener> connectionListeners = Collections
            .synchronizedSet(new HashSet<ConnectionListener>());

    private Set<TranscriptListener> transcriptListeners = Collections
            .synchronizedSet(new HashSet<TranscriptListener>());

    private Set<ProgressListener> progressListeners = Collections.synchronizedSet(new HashSet<ProgressListener>());

    private Set<ErrorListener> errorListeners = Collections.synchronizedSet(new HashSet<ErrorListener>());

    /**
     * Connection attempt being made.
     */
    private boolean opening;

    /**
     * Delegating CloudFront requests.
     */
    private DistributionConfiguration cf;

    protected Session(Host h) {
        this.host = h;
        this.ua = new PreferencesUseragentProvider();
    }

    /**
     * @param <C> Native client type
     * @return The client implementation.
     * @throws ConnectionCanceledException If the connection is alreay closed
     */
    protected abstract <C> C getClient() throws ConnectionCanceledException;

    public String getUserAgent() {
        return ua.get();
    }

    /**
     * Assert that the connection to the remote host is still alive.
     * Open connection if needed.
     *
     * @throws IOException The connection to the remote host failed.
     */
    public void check() throws IOException {
        try {
            try {
                if (!this.isConnected()) {
                    if (StringUtils.isBlank(host.getHostname())) {
                        if (StringUtils.isBlank(host.getProtocol().getDefaultHostname())) {
                            log.warn(String.format("No default hostname configured for protocol %s",
                                    host.getProtocol()));
                            throw new ConnectionCanceledException();
                        }
                        // If hostname is missing update with default
                        host.setHostname(host.getProtocol().getDefaultHostname());
                    }
                    // If not connected anymore, reconnect the session
                    this.connect();
                } else {
                    // The session is still supposed to be connected
                    try {
                        // Send a 'no operation command' to make sure the session is alive
                        this.noop();
                    } catch (IOException e) {
                        // Close the underlying socket first
                        this.interrupt();
                        // Try to reconnect once more
                        this.connect();
                    }
                }
            } catch (SocketException e) {
                if (e.getMessage().equals("Software caused connection abort")) {
                    // Do not report as failed if socket opening interrupted
                    log.warn("Supressed socket exception:" + e.getMessage());
                    throw new ConnectionCanceledException(e.getMessage(), e);
                }
                if (e.getMessage().equals("Socket closed")) {
                    // Do not report as failed if socket opening interrupted
                    log.warn("Supressed socket exception:" + e.getMessage());
                    throw new ConnectionCanceledException(e.getMessage(), e);
                }
                throw e;
            } catch (SSLHandshakeException e) {
                log.error(String.format("SSL Handshake failed for host %s", host), e);
                if (e.getCause() instanceof sun.security.validator.ValidatorException) {
                    throw e;
                }
                // Most probably caused by user dismissing ceritifcate. No trusted certificate found.
                throw new ConnectionCanceledException(e.getMessage(), e);
            }
        } catch (IOException e) {
            this.interrupt();
            this.error("Connection failed", e);
            throw e;
        }
    }

    /**
     * @return The timeout in milliseconds
     */
    protected int timeout() {
        return Preferences.instance().getInteger("connection.timeout.seconds") * 1000;
    }

    /**
     * No information about the curren state of the connection but only the protocol.
     *
     * @return True if the control channel is either tunneled using TLS or SSH
     */
    public boolean isSecure() {
        if (this.isConnected()) {
            return this.host.getProtocol().isSecure();
        }
        return false;
    }

    /**
     * Opens the TCP connection to the server
     *
     * @throws IOException            I/O failure
     * @throws LoginCanceledException Login prompt dismissed with cancel
     */
    protected abstract void connect() throws IOException;

    /**
     * Prompt for username and password if not available.
     *
     * @param controller Prompt
     * @throws LoginCanceledException Login prompt dismissed with cancel
     */
    protected void prompt(final LoginController controller) throws IOException {
        controller.check(host, Locale.localizedString("Login with username and password", "Credentials"), null);
    }

    public boolean isUnsecurewarning() {
        return unsecurewarning;
    }

    public void setUnsecurewarning(boolean unsecurewarning) {
        this.unsecurewarning = unsecurewarning;
    }

    /**
     * Attempts to login using the credentials provided from the login controller.
     *
     * @throws IOException I/O failure
     */
    protected void login() throws IOException {
        this.login(LoginControllerFactory.get(this));
    }

    /**
     * @param controller Prompt
     * @throws IOException I/O failure
     */
    protected void login(final LoginController controller) throws IOException {
        this.prompt(controller);

        final Credentials credentials = host.getCredentials();
        this.warn(controller, credentials);

        this.message(MessageFormat.format(Locale.localizedString("Authenticating as {0}", "Status"),
                credentials.getUsername()));
        this.login(controller, credentials);

        if (!this.isConnected()) {
            throw new ConnectionCanceledException();
        }
        controller.success(host);
    }

    /**
     * Warning if credenials are sent plaintext.
     *
     * @param login       Prompt
     * @param credentials Login credentials
     * @throws ConnectionCanceledException If connection should be dropped
     */
    protected void warn(final LoginController login, final Credentials credentials) throws IOException {
        if (this.isUnsecurewarning() && !host.getProtocol().isSecure() && !credentials.isAnonymousLogin()
                && !Preferences.instance().getBoolean("connection.unsecure." + host.getHostname())) {
            try {
                login.warn(
                        MessageFormat.format(Locale.localizedString("Unsecured {0} connection", "Credentials"),
                                host.getProtocol().getName()),
                        MessageFormat.format(
                                Locale.localizedString("{0} will be sent in plaintext.", "Credentials"),
                                credentials.getPasswordPlaceholder()),
                        "connection.unsecure." + host.getHostname());
            } finally {
                // Do not warn again upon subsequent login
                this.setUnsecurewarning(false);
            }
        }
    }

    /**
     * Send the authentication credentials to the server. The connection must be opened first.
     *
     * @param controller  Prompt
     * @param credentials Login credentials
     * @throws IOException            I/O failure
     * @throws LoginCanceledException
     * @see #connect
     */
    protected abstract void login(LoginController controller, Credentials credentials) throws IOException;

    /**
     * Mount the default path of the configured host or the home directory as returned by the server
     * when not given.
     *
     * @return Null if mount fails. Check the error listener for details.
     */
    public Path mount() {
        this.message(MessageFormat.format(Locale.localizedString("Mounting {0}", "Status"), host.getHostname()));
        try {
            this.check();
            if (!this.isConnected()) {
                return null;
            }
            Path directory = this.home();
            // Retrieve direcotry listing of default path
            if (!directory.children().attributes().isReadable()) {
                // The default path does not exist or is not readable due to possible permission issues
                // Fallback to default working directory
                directory = this.workdir();
            }
            return directory;
        } catch (IOException e) {
            log.warn(String.format("Connection failed to host %s:%s", host, e.getMessage()));
            return null;
        } finally {
            // Reset current working directory in bookmark
            host.setWorkdir(null);
        }
    }

    /**
     * @return Home directory
     * @throws IOException I/O failure
     */
    public Path home() throws IOException {
        final String directory;
        if (StringUtils.isNotBlank(host.getWorkdir())) {
            directory = host.getWorkdir();
        } else if (StringUtils.isNotBlank(host.getDefaultPath())) {
            if (host.getDefaultPath().startsWith(String.valueOf(Path.DELIMITER))) {
                // Mount absolute path
                directory = host.getDefaultPath();
            } else {
                Path workdir = this.workdir();
                if (host.getDefaultPath().startsWith(Path.HOME)) {
                    // Relative path to the home directory
                    return PathFactory.createPath(this, workdir.getAbsolute(), host.getDefaultPath().substring(1),
                            Path.DIRECTORY_TYPE);
                } else {
                    // Relative path
                    return PathFactory.createPath(this, workdir.getAbsolute(), host.getDefaultPath(),
                            Path.DIRECTORY_TYPE);
                }
            }
        } else {
            // No default path configured
            return this.workdir();
        }
        return PathFactory.createPath(this, directory,
                directory.equals(String.valueOf(Path.DELIMITER)) ? Path.VOLUME_TYPE | Path.DIRECTORY_TYPE
                        : Path.DIRECTORY_TYPE);
    }

    /**
     * @return The current working directory (pwd) or null if it cannot be retrieved for whatever reason
     * @throws ConnectionCanceledException If the underlying connection has already been closed before
     */
    public Path workdir() throws IOException {
        return PathFactory.createPath(this, String.valueOf(Path.DELIMITER), Path.VOLUME_TYPE | Path.DIRECTORY_TYPE);
    }

    /**
     * Close the connecion to the remote host. The protocol specific
     * implementation has to be implemented in the subclasses. Subsequent calls to #getClient() must return null.
     *
     * @see #isConnected()
     */
    public abstract void close();

    /**
     * @return the host this session connects to
     */
    public Host getHost() {
        return this.host;
    }

    /**
     * @return The custom character encoding specified by the host
     *         of this session or the default encoding if not specified
     * @see Preferences
     * @see Host
     */
    public String getEncoding() {
        if (null == this.host.getEncoding()) {
            return Preferences.instance().getProperty("browser.charset.encoding");
        }
        return this.host.getEncoding();
    }

    /**
     * @return The maximum number of concurrent connections allowed or -1 if no limit is set
     */
    public int getMaxConnections() {
        if (null == host.getMaxConnections()) {
            return Preferences.instance().getInteger("connection.host.max");
        }
        return host.getMaxConnections();
    }

    /**
     * @param workdir The workdir to create query
     * @return True if making directories is possible.
     * @see Path#mkdir()
     */
    public boolean isCreateFolderSupported(final Path workdir) {
        return true;
    }

    /**
     * @param workdir The workdir to create query
     * @return True if creating an empty file is possible.
     * @see ch.cyberduck.core.Path#touch()
     */
    public boolean isCreateFileSupported(final Path workdir) {
        return true;
    }

    public boolean isRenameSupported(Path file) {
        return true;
    }

    /**
     * @return True if ACLs are supported
     * @see ch.cyberduck.core.Path#writeAcl(Acl, boolean)
     * @see Path#readAcl()
     */
    public boolean isAclSupported() {
        return false;
    }

    /**
     * @return True if UNIX permissions can be read and written.
     */
    public boolean isUnixPermissionsSupported() {
        return true;
    }

    /**
     * @return True if timestamp of file can be read.
     * @see AbstractPath#writeTimestamp(long, long, long)
     * @see ch.cyberduck.core.Path#readTimestamp()
     */
    public boolean isReadTimestampSupported() {
        return true;
    }

    /**
     * @return True if timestamp of file can be read and written.
     * @see AbstractPath#writeTimestamp(long, long, long)
     * @see ch.cyberduck.core.Path#readTimestamp()
     */
    public boolean isWriteTimestampSupported() {
        return true;
    }

    /**
     * @return MD5/ETag available for files.
     */
    public boolean isChecksumSupported() {
        return false;
    }

    /**
     * @return True if files can be reverted
     */
    public boolean isRevertSupported() {
        return false;
    }

    /**
     * Send a 'no operation' command
     *
     * @throws IOException I/O failure
     */
    protected void noop() throws IOException {
        //
    }

    /**
     * Interrupt any running operation asynchroneously by closing the underlying socket.
     * Close the underlying socket regardless of its state; will throw a socket exception
     * on the thread owning the socket
     */
    public void interrupt() {
        this.close();
    }

    /**
     * @return True if command execution if supported by the protocol.
     */
    public boolean isSendCommandSupported() {
        return false;
    }

    /**
     * Sends an arbitrary command to the server
     *
     * @param command Command to send
     * @throws IOException I/O failure
     * @see #isSendCommandSupported()
     */
    public void sendCommand(String command) throws IOException {
        throw new UnsupportedOperationException();
    }

    /**
     * @return False
     */
    public boolean isArchiveSupported() {
        return false;
    }

    /**
     * Create ompressed archive.
     *
     * @param archive Archive format description
     * @param files   List of files to archive
     */
    public void archive(final Archive archive, final List<Path> files) {
        try {
            this.check();

            this.sendCommand(archive.getCompressCommand(files));
        } catch (IOException e) {
            this.error("Cannot create archive", e);
        }
    }

    /**
     * @return True if archiving is supported. Always false
     */
    public boolean isUnarchiveSupported() {
        return false;
    }

    /**
     * Unpack compressed archive
     *
     * @param archive Archive format description
     * @param file    File to decompress
     */
    public void unarchive(final Archive archive, final Path file) {
        try {
            this.check();

            this.sendCommand(archive.getDecompressCommand(file));
        } catch (IOException e) {
            this.error("Cannot expand archive", e);
        }
    }

    /**
     * @return boolean True if the session has not yet been closed.
     */
    public boolean isConnected() {
        try {
            this.getClient();
        } catch (ConnectionCanceledException e) {
            return false;
        }
        return true;
    }

    /**
     * @return True if a connection attempt is currently being made. False if the connection
     *         has already been established or is closed.
     */
    public boolean isOpening() {
        return opening;
    }

    public void addConnectionListener(final ConnectionListener listener) {
        connectionListeners.add(listener);
    }

    public void removeConnectionListener(final ConnectionListener listener) {
        connectionListeners.remove(listener);
    }

    /**
     * Notifies all connection listeners that an attempt is made to open this session
     *
     * @throws ResolveCanceledException      If the name resolution has been canceled by the user
     * @throws java.net.UnknownHostException If the name resolution failed
     * @see ConnectionListener
     */
    protected void fireConnectionWillOpenEvent() throws ResolveCanceledException, UnknownHostException {
        if (log.isDebugEnabled()) {
            log.debug(String.format("Connection will open to %s", host));
        }
        ConnectionListener[] l = connectionListeners.toArray(new ConnectionListener[connectionListeners.size()]);
        for (ConnectionListener listener : l) {
            listener.connectionWillOpen();
        }

        // Update status flag
        opening = true;

        // Configuring proxy if any
        ProxyFactory.get().configure(host);

        final Resolver resolver = new Resolver(
                HostnameConfiguratorFactory.get(host.getProtocol()).lookup(host.getHostname()));
        this.message(MessageFormat.format(Locale.localizedString("Resolving {0}", "Status"), host.getHostname()));

        // Try to resolve the hostname first
        resolver.resolve();
        // The IP address could successfully be determined
        this.message(MessageFormat.format(Locale.localizedString("Opening {0} connection to {1}", "Status"),
                host.getProtocol().getName(), host.getHostname()));
    }

    /**
     * Starts the <code>KeepAliveTask</code> if <code>connection.keepalive</code> is true
     * Notifies all connection listeners that the connection has been opened successfully
     *
     * @see ConnectionListener
     */
    protected void fireConnectionDidOpenEvent() {
        if (log.isDebugEnabled()) {
            log.debug(String.format("Connection did open to %s", host));
        }
        // Update last accessed timestamp
        host.setTimestamp(new Date());
        // Update status flag
        opening = false;

        final HistoryCollection history = HistoryCollection.defaultCollection();
        history.add(new Host(host.getAsDictionary()));

        for (ConnectionListener listener : connectionListeners
                .toArray(new ConnectionListener[connectionListeners.size()])) {
            listener.connectionDidOpen();
        }
        this.message(MessageFormat.format(Locale.localizedString("{0} connection opened", "Status"),
                host.getProtocol().getName()));
    }

    /**
     * Notifes all connection listeners that a connection is about to be closed
     *
     * @see ConnectionListener
     */
    protected void fireConnectionWillCloseEvent() {
        if (log.isDebugEnabled()) {
            log.debug(String.format("Connection will close to %s", host));
        }
        this.message(
                MessageFormat.format(Locale.localizedString("Disconnecting {0}", "Status"), host.getHostname()));
        for (ConnectionListener listener : connectionListeners
                .toArray(new ConnectionListener[connectionListeners.size()])) {
            listener.connectionWillClose();
        }
    }

    /**
     * Notifes all connection listeners that a connection has been closed
     *
     * @see ConnectionListener
     */
    protected void fireConnectionDidCloseEvent() {
        if (log.isDebugEnabled()) {
            log.debug(String.format("Connection did close to %s", host));
        }
        this.cdn().clear();

        for (ConnectionListener listener : connectionListeners
                .toArray(new ConnectionListener[connectionListeners.size()])) {
            listener.connectionDidClose();
        }
        // Update status flag
        opening = false;
    }

    public void addTranscriptListener(final TranscriptListener listener) {
        transcriptListeners.add(listener);
    }

    public void removeTranscriptListener(final TranscriptListener listener) {
        transcriptListeners.remove(listener);
    }

    /**
     * Log the message to all subscribed transcript listeners
     *
     * @param message Log line
     * @see TranscriptListener
     */
    @Override
    public void log(final boolean request, final String message) {
        if (log.isInfoEnabled()) {
            log.info(message);
        }
        for (TranscriptListener listener : transcriptListeners
                .toArray(new TranscriptListener[transcriptListeners.size()])) {
            listener.log(request, message);
        }
    }

    /**
     * Content Range support
     *
     * @return True if skipping is supported
     */
    public boolean isDownloadResumable() {
        return true;
    }

    /**
     * Content Range support
     *
     * @return True if appending is supported
     */
    public boolean isUploadResumable() {
        return true;
    }

    /**
     * @return True if symbolic links are supported on UNIX filesystems
     */
    public boolean isCreateSymlinkSupported() {
        return false;
    }

    /**
     * @return List of known ACL users
     */
    public List<Acl.User> getAvailableAclUsers() {
        return Collections.emptyList();
    }

    /**
     * @return If metadata for files are supported
     */
    public boolean isMetadataSupported() {
        return false;
    }

    /**
     * @return If CDN distribution configuration is supported
     * @see #cdn()
     */
    public boolean isCDNSupported() {
        return true;
    }

    public IdentityConfiguration iam() {
        return new DefaultCDNCredentialsIdentityConfiguration(host);
    }

    public boolean isAnalyticsSupported() {
        return this.isCDNSupported();
    }

    public AnalyticsProvider analytics() {
        return new QloudstatAnalyticsProvider();
    }

    public DistributionConfiguration cdn() {
        if (null == cf) {
            cf = new CloudFrontDistributionConfiguration(LoginControllerFactory.get(this),
                    new DefaultCDNCredentialsIdentityConfiguration(host).getUserCredentials(null),
                    new ErrorListener() {
                        @Override
                        public void error(BackgroundException exception) {
                            Session.this.error(exception);
                        }
                    }, new ProgressListener() {
                        @Override
                        public void message(String message) {
                            Session.this.message(message);
                        }
                    }, new TranscriptListener() {
                        @Override
                        public void log(boolean request, String message) {
                            Session.this.log(request, message);
                        }
                    }) {

                /**
                 * @return Service name of the CDN
                 */
                public String getName() {
                    if (this.isCDNSupported()) {
                        return super.getName();
                    }
                    return Locale.localizedString("None");
                }

                @Override
                public List<Distribution.Method> getMethods(final String container) {
                    if (this.isCDNSupported()) {
                        return Arrays.asList(Distribution.CUSTOM);
                    }
                    return Collections.emptyList();
                }

                @Override
                public String getOrigin(final Distribution.Method method, final String container) {
                    if (Distribution.CUSTOM.equals(method)) {
                        try {
                            return new URI(Session.this.getHost().getWebURL()).getHost();
                        } catch (URISyntaxException e) {
                            log.error(String.format("Failure parsing URI %s", Session.this.getHost().getWebURL()),
                                    e);
                        }
                    }
                    return super.getOrigin(method, container);
                }
            };
        }
        return cf;
    }

    /**
     * Roles available for users in a configurable ACL.
     *
     * @param files List of files
     * @return A list of role names.
     */
    public List<Acl.Role> getAvailableAclRoles(final List<Path> files) {
        return Collections.emptyList();
    }

    public void addProgressListener(final ProgressListener listener) {
        progressListeners.add(listener);
    }

    public void removeProgressListener(final ProgressListener listener) {
        progressListeners.remove(listener);
    }

    /**
     * Notifies all progress listeners
     *
     * @param message The message to be displayed in a status field
     * @see ProgressListener
     */
    public void message(final String message) {
        if (log.isInfoEnabled()) {
            log.info(message);
        }
        for (ProgressListener listener : progressListeners
                .toArray(new ProgressListener[progressListeners.size()])) {
            listener.message(message);
        }
    }

    public void addErrorListener(final ErrorListener listener) {
        errorListeners.add(listener);
    }

    public void removeErrorListener(final ErrorListener listener) {
        errorListeners.remove(listener);
    }

    public void error(String message, Throwable e) {
        this.error(null, message, e);
    }

    /**
     * Notifies all error listeners of this error without sending this error to Growl
     *
     * @param path    The path related to this error
     * @param message The error message to be displayed in the alert sheet
     * @param e       The cause of the error
     */
    public void error(final Path path, final String message, final Throwable e) {
        this.error(new BackgroundException(this.getHost(), path, message, e));
    }

    public void error(final BackgroundException failure) {
        this.message(failure.getMessage());
        for (ErrorListener listener : errorListeners.toArray(new ErrorListener[errorListeners.size()])) {
            listener.error(failure);
        }
    }

    /**
     * @return The directory listing cache for this session
     */
    public Cache cache() {
        return this.cache;
    }

    /**
     * @param other Session instance
     * @return true if the other session denotes the same hostname and protocol
     */
    @Override
    public boolean equals(Object other) {
        if (null == other) {
            return false;
        }
        if (other instanceof Session) {
            return host.getHostname().equals(((Session) other).getHost().getHostname())
                    && host.getProtocol().equals(((Session) other).getHost().getProtocol());
        }
        return false;
    }

    @Override
    public int hashCode() {
        int result = host.getHostname() != null ? host.getHostname().hashCode() : 0;
        result = 31 * result + (host.getProtocol() != null ? host.getProtocol().hashCode() : 0);
        return result;
    }

    public String toString() {
        return String.format("Session %s", host.toURL());
    }

    //Metodos para teste de interface
    public List<String> getIdps() {
        List<String> idpList = new ArrayList<String>();
        idpList.add("idp.ufrn.br");
        idpList.add("idp.ufpa.br");
        idpList.add("idp.ufcg.br");
        return idpList;
    }

}