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.i18n.Locale;
import ch.cyberduck.LoginController;
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.UnknownHostException;
import java.text.MessageFormat;
import java.util.*;
/**
* @version $Id: Session.java 5830 2010-03-03 12:02:26Z dkocher $
*/
public abstract class Session {
private static Logger log = Logger.getLogger(Session.class);
/**
* Encapsulating all the information of the remote host
* @uml.property name="host"
* @uml.associationEnd
*/
protected Host host;
/**
* @uml.property name="workdir"
* @uml.associationEnd
*/
protected Path workdir;
protected Session(Host h) {
this.host = h;
}
protected abstract <C> C getClient() throws ConnectionCanceledException;
/**
* @return The remote host identification such as the response to the SYST command in FTP
*/
public String getIdentification() {
try {
return this.host.getIp();
}
catch(UnknownHostException e) {
return this.host.getHostname();
}
}
/**
* @uml.property name="ua"
*/
private final String ua = Preferences.instance().getProperty("application") + "/"
+ Preferences.instance().getProperty("version")
+ " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + ")"
+ " (" + System.getProperty("os.arch") + ")";
public String getUserAgent() {
return ua;
}
/**
* 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 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();
}
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();
}
throw e;
}
catch(SSLHandshakeException e) {
log.error("SSL Handshake failed: " + e.getMessage());
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());
}
host.setTimestamp(new Date());
}
catch(IOException e) {
this.interrupt();
this.error("Connection failed", e);
throw e;
}
}
/**
* @return The timeout in milliseconds
*/
protected int timeout() {
return (int) Preferences.instance().getDouble("connection.timeout.seconds") * 1000;
}
/**
* @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
* @throws LoginCanceledException
*/
protected abstract void connect() throws IOException, ConnectionCanceledException, LoginCanceledException;
/**
* @uml.property name="login"
* @uml.associationEnd
*/
protected LoginController login;
/**
* Sets the callback to ask for login credentials
*
* @param loginController
* @see #login
*/
public void setLoginController(LoginController loginController) {
this.login = loginController;
}
protected void login() throws IOException {
login.check(host);
final Credentials credentials = host.getCredentials();
// this.message(MessageFormat.format(Locale.localizedString("Authenticating as {0}", "Status"),
// credentials.getUsername()));
this.message(credentials.getUsername());
this.login(credentials);
if(!this.isConnected()) {
throw new ConnectionCanceledException();
}
login.success(host);
}
/**
* Send the authentication credentials to the server. The connection must be opened first.
*
* @throws IOException
* @throws LoginCanceledException
* @see #connect
*/
protected abstract void login(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() {
try {
if(StringUtils.isNotBlank(host.getDefaultPath())) {
return this.mount(host.getDefaultPath());
}
return this.mount(null);
}
catch(IOException e) {
this.interrupt();
}
return null;
}
/**
* Connect to the remote host and mount the home directory
*
* @param directory
* @return null if we fail, the mounted working directory if we succeed
*/
protected Path mount(String directory) throws IOException {
// this.message(MessageFormat.format(Locale.localizedString("Mounting {0}", "Status"),
// host.getHostname()));
this.message(host.getHostname());
this.check();
if(!this.isConnected()) {
return null;
}
Path home;
if(directory != null) {
if(directory.startsWith(Path.DELIMITER) || directory.equals(this.workdir().getName())) {
home = PathFactory.createPath(this, directory,
directory.equals(Path.DELIMITER) ? Path.VOLUME_TYPE | Path.DIRECTORY_TYPE : Path.DIRECTORY_TYPE);
}
else if(directory.startsWith(Path.HOME)) {
// relative path to the home directory
home = PathFactory.createPath(this,
this.workdir().getAbsolute(), directory.substring(1), Path.DIRECTORY_TYPE);
}
else {
// relative path
home = PathFactory.createPath(this,
this.workdir().getAbsolute(), directory, Path.DIRECTORY_TYPE);
}
if(!home.childs().attributes().isReadable()) {
// the default path does not exist or is not readable due to permission issues
home = this.workdir();
}
}
else {
home = this.workdir();
}
return home;
}
/**
* 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
* @uml.property name="host"
*/
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();
}
/**
* @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 {
if(!this.isConnected()) {
throw new ConnectionCanceledException();
}
if(null == workdir) {
workdir = PathFactory.createPath(this, Path.DELIMITER, Path.VOLUME_TYPE | Path.DIRECTORY_TYPE);
}
return workdir;
}
/**
* @param workdir
* @throws IOException
* @uml.property name="workdir"
*/
public void setWorkdir(Path workdir) throws IOException {
if(!this.isConnected()) {
throw new ConnectionCanceledException();
}
this.workdir = workdir;
}
/**
* Send a 'no operation' command
*
* @throws IOException
*/
protected abstract 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();
}
public boolean isSendCommandSupported() {
return false;
}
/**
* Sends an arbitrary command to the server
*
* @param command
*/
public abstract void sendCommand(String command) throws IOException;
/**
* @return False
*/
public boolean isArchiveSupported() {
return false;
}
/**
* Create ompressed archive.
*
* @param archive
*/
public void archive(final Archive archive, final List<Path> files) {
try {
this.check();
this.sendCommand(archive.getCompressCommand(files));
// The directory listing is no more current
for(Path file : files) {
file.getParent().invalidate();
}
}
catch(IOException e) {
this.error("Cannot create archive", e);
}
}
/**
* @return False
*/
public boolean isUnarchiveSupported() {
return false;
}
/**
* Unpack compressed archive
*
* @param archive
*/
public void unarchive(final Archive archive, Path file) {
try {
this.check();
this.sendCommand(archive.getDecompressCommand(file));
// The directory listing is no more current
file.getParent().invalidate();
}
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;
}
/**
* @uml.property name="opening"
*/
private boolean opening;
/**
* If a connection attempt is currently being made.
* @return
* @uml.property name="opening"
*/
public boolean isOpening() {
return opening;
}
/**
* @uml.property name="connectionListeners"
* @uml.associationEnd multiplicity="(0 -1)" elementType="ch.cyberduck.core.ConnectionListener"
*/
private Set<ConnectionListener> connectionListeners
= Collections.synchronizedSet(new HashSet<ConnectionListener>());
public void addConnectionListener(ConnectionListener listener) {
connectionListeners.add(listener);
}
public void removeConnectionListener(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 {
log.debug("connectionWillOpen");
ConnectionListener[] l = connectionListeners.toArray(new ConnectionListener[connectionListeners.size()]);
for(ConnectionListener listener : l) {
listener.connectionWillOpen();
}
// Configuring proxy if any
// ProxyFactory.instance().configure(host);
Resolver resolver = new Resolver(this.host.getHostname(true));
// this.message(MessageFormat.format(Locale.localizedString("Resolving {0}", "Status"),
// host.getHostname()));
this.message(host.getHostname());
// Try to resolve the hostname first
resolver.resolve();
// The IP address could successfully be determined
}
/**
* 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() {
log.debug("connectionDidOpen");
for(ConnectionListener listener : connectionListeners.toArray(new ConnectionListener[connectionListeners.size()])) {
listener.connectionDidOpen();
}
}
/**
* Notifes all connection listeners that a connection is about to be closed
*
* @see ConnectionListener
*/
protected void fireConnectionWillCloseEvent() {
log.debug("connectionWillClose");
// this.message(MessageFormat.format(Locale.localizedString("Disconnecting {0}", "Status"),
// this.getHost().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() {
log.debug("connectionDidClose");
this.workdir = null;
for(ConnectionListener listener : connectionListeners.toArray(new ConnectionListener[connectionListeners.size()])) {
listener.connectionDidClose();
}
}
/**
* @uml.property name="transcriptListeners"
* @uml.associationEnd multiplicity="(0 -1)" elementType="ch.cyberduck.core.TranscriptListener"
*/
private Set<TranscriptListener> transcriptListeners
= Collections.synchronizedSet(new HashSet<TranscriptListener>());
public void addTranscriptListener(TranscriptListener listener) {
transcriptListeners.add(listener);
}
public void removeTranscriptListener(TranscriptListener listener) {
transcriptListeners.remove(listener);
}
/**
* Log the message to all subscribed transcript listeners
*
* @param message
* @see TranscriptListener
*/
public void log(boolean request, final String message) {
log.info(message);
for(TranscriptListener listener : transcriptListeners) {
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;
}
/**
* @uml.property name="progressListeners"
* @uml.associationEnd multiplicity="(0 -1)" elementType="ch.cyberduck.core.ProgressListener"
*/
private Set<ProgressListener> progressListeners
= Collections.synchronizedSet(new HashSet<ProgressListener>());
public void addProgressListener(ProgressListener listener) {
progressListeners.add(listener);
}
public void removeProgressListener(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) {
log.info(message);
for(ProgressListener listener : progressListeners.toArray(new ProgressListener[progressListeners.size()])) {
listener.message(message);
}
}
/**
* @uml.property name="errorListeners"
* @uml.associationEnd multiplicity="(0 -1)" elementType="ch.cyberduck.core.ErrorListener"
*/
private Set<ErrorListener> errorListeners
= Collections.synchronizedSet(new HashSet<ErrorListener>());
public void addErrorListener(ErrorListener listener) {
errorListeners.add(listener);
}
public void removeErrorListener(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(Path path, String message, Throwable e) {
final BackgroundException failure = new BackgroundException(this, path, message, e);
this.message(failure.getMessage());
for(ErrorListener listener : errorListeners.toArray(new ErrorListener[errorListeners.size()])) {
listener.error(failure);
}
}
/**
* Caching files listings of previously visited directories
* @uml.property name="cache"
* @uml.associationEnd
*/
private Cache<Path> cache;
/**
* @return The directory listing cache
*/
public Cache<Path> cache() {
if(null == cache) {
cache = new Cache<Path>() {
@Override
public String toString() {
return "Cache for " + Session.this.toString();
}
};
}
return this.cache;
}
/**
* @param other
* @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 this.getHost().getHostname().equals(((Session) other).getHost().getHostname())
&& this.getHost().getProtocol().equals(((Session) other).getHost().getProtocol());
}
return false;
}
public String toString() {
return "Session " + host.toURL();
}
}