de.dal33t.powerfolder.clientserver.ServerClient.java Source code

Java tutorial

Introduction

Here is the source code for de.dal33t.powerfolder.clientserver.ServerClient.java

Source

/*
 * Copyright 2004 - 2008 Christian Sprajc. All rights reserved.
 *
 * This file is part of PowerFolder.
 *
 * PowerFolder 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.
 *
 * PowerFolder 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with PowerFolder. If not, see <http://www.gnu.org/licenses/>.
 *
 * $Id$
 */
package de.dal33t.powerfolder.clientserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.PrivilegedExceptionAction;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.DefaultHttpClient;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;

import com.sun.security.auth.callback.TextCallbackHandler;

import de.dal33t.powerfolder.ConfigurationEntry;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Controller;
import de.dal33t.powerfolder.Member;
import de.dal33t.powerfolder.PFComponent;
import de.dal33t.powerfolder.PreferencesEntry;
import de.dal33t.powerfolder.disk.Folder;
import de.dal33t.powerfolder.event.FolderRepositoryEvent;
import de.dal33t.powerfolder.event.FolderRepositoryListener;
import de.dal33t.powerfolder.event.ListenerSupportFactory;
import de.dal33t.powerfolder.event.NodeManagerAdapter;
import de.dal33t.powerfolder.event.NodeManagerEvent;
import de.dal33t.powerfolder.light.AccountInfo;
import de.dal33t.powerfolder.light.FileInfo;
import de.dal33t.powerfolder.light.FolderInfo;
import de.dal33t.powerfolder.light.MemberInfo;
import de.dal33t.powerfolder.light.ServerInfo;
import de.dal33t.powerfolder.message.FolderList;
import de.dal33t.powerfolder.message.Identity;
import de.dal33t.powerfolder.message.clientserver.AccountDetails;
import de.dal33t.powerfolder.net.ConnectionHandler;
import de.dal33t.powerfolder.net.ConnectionListener;
import de.dal33t.powerfolder.security.Account;
import de.dal33t.powerfolder.security.AdminPermission;
import de.dal33t.powerfolder.security.AnonymousAccount;
import de.dal33t.powerfolder.security.AuthenticationFailedException;
import de.dal33t.powerfolder.security.FolderCreatePermission;
import de.dal33t.powerfolder.security.NotLoggedInException;
import de.dal33t.powerfolder.security.SecurityException;
import de.dal33t.powerfolder.util.Base64;
import de.dal33t.powerfolder.util.ConfigurationLoader;
import de.dal33t.powerfolder.util.IdGenerator;
import de.dal33t.powerfolder.util.LoginUtil;
import de.dal33t.powerfolder.util.PathUtils;
import de.dal33t.powerfolder.util.ProUtil;
import de.dal33t.powerfolder.util.Reject;
import de.dal33t.powerfolder.util.StringUtils;
import de.dal33t.powerfolder.util.Translation;
import de.dal33t.powerfolder.util.Util;
import de.dal33t.powerfolder.util.Waiter;
import de.dal33t.powerfolder.util.net.NetworkUtil;
import edu.kit.scc.dei.ecplean.ECPAuthenticationException;
import edu.kit.scc.dei.ecplean.ECPAuthenticator;
import edu.kit.scc.dei.ecplean.ECPUnauthorizedException;

/**
 * Client to a server.
 *
 * @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc</a>
 * @version $Revision: 1.5 $
 */
public class ServerClient extends PFComponent {
    private static final String MEMBER_ID_TEMP_PREFIX = "TEMP_IDENTITY_";

    /**
     * If the current thread which processes Member.handleMessage is the server.
     */
    public static final ThreadLocal<Boolean> SERVER_HANDLE_MESSAGE_THREAD = new ThreadLocal<Boolean>() {
        @Override
        protected Boolean initialValue() {
            return Boolean.FALSE;
        }
    };

    // The last used username and password.
    // Tries to re-login with these if re-connection happens
    private String username;
    private String passwordObf;

    private String shibUsername;
    private String shibToken;

    private Member server;
    private final MyThrowableHandler throwableHandler = new MyThrowableHandler();
    private final AtomicBoolean loggingIn = new AtomicBoolean();
    private final AtomicBoolean loginExecuted = new AtomicBoolean(false);
    /**
     * PFC-2589: Don't auto login, if the last login was unsuccessfull
     */
    private final AtomicBoolean lastLoginSuccessful = new AtomicBoolean(true);

    /**
     * ONLY FOR TESTS: If this client should connect to the server where it is
     * assigned to.
     */
    private boolean allowServerChange;

    // Prevent HAMMERING on the cluster.
    private int recentServerSwitches;
    private Date recentServerSwitch;
    /**
     * Update the config with new HOST/ID infos if retrieved from server.
     */
    private boolean updateConfig;

    /**
     * #2366: Quick login during handshake supported
     */
    private boolean supportsQuickLogin;

    /**
     * Log that is kept to synchronize calls to login
     */
    private final Object loginLock = new Object();

    /**
     * PFC-2534: remember last IdP and number of login retries to skip after
     * unauthorized login try
     */
    private String lastIdPUsed;
    private int shibbolethUnauthRetriesSkip;

    private AccountDetails accountDetails;

    private SecurityService securityService;
    private AccountService userService;
    private FolderService folderService;
    private PublicKeyService publicKeyService;

    private ServerClientListener listenerSupport;

    // Construction ***********************************************************

    /**
     * Constructs a server client with the defaults from the config. allows
     * server change.
     *
     * @param controller
     */
    public ServerClient(Controller controller) {
        super(controller);
        String name = ConfigurationEntry.SERVER_NAME.getValue(controller);
        String host = ConfigurationEntry.SERVER_HOST.getValue(controller);
        String nodeId = ConfigurationEntry.SERVER_NODEID.getValue(controller);

        if (!ConfigurationEntry.SERVER_NODEID.hasValue(controller)) {
            if (ConfigurationEntry.SERVER_HOST.hasValue(controller)) {
                // Hostname set, but no node id?
                nodeId = null;
            }
        }

        boolean allowServerChange = true;
        boolean updateConfig = ConfigurationEntry.SERVER_CONFIG_UPDATE.getValueBoolean(controller);

        init(controller, name, host, nodeId, allowServerChange, updateConfig);
    }

    public ServerClient(Controller controller, String name, String host, String nodeId, boolean allowServerChange,
            boolean updateConfig) {
        super(controller);
        init(controller, name, host, nodeId, allowServerChange, updateConfig);
    }

    /**
     * Constructs a server client with the defaults from the config.
     *
     * @param controller
     * @param name
     * @param host
     * @param nodeId
     * @param allowServerChange
     * @param updateConfig
     */
    private void init(Controller controller, String name, String host, String nodeId, boolean allowServerChange,
            boolean updateConfig) {
        this.allowServerChange = allowServerChange;
        this.updateConfig = updateConfig;
        supportsQuickLogin = true;

        // Custom server
        String theName = StringUtils.isBlank(name) ? Translation.getTranslation("online_storage.connecting") : name;

        boolean temporaryNode = StringUtils.isBlank(nodeId);
        String theNodeId = temporaryNode ? MEMBER_ID_TEMP_PREFIX + '|' + IdGenerator.makeId() : nodeId;
        Member theNode = controller.getNodeManager().getNode(theNodeId);
        if (theNode == null) {
            String networkId = getController().getNodeManager().getNetworkId();
            MemberInfo serverNode = new MemberInfo(theName, theNodeId, networkId);
            if (temporaryNode) {
                // Temporary node. Don't add to nodemanager
                theNode = new Member(getController(), serverNode);
            } else {
                theNode = serverNode.getNode(getController(), true);
            }
        }
        if (StringUtils.isNotBlank(host)) {
            theNode.getInfo().setConnectAddress(Util.parseConnectionString(host));
        }

        if (theNode.getReconnectAddress() == null) {
            logSevere("Got server without reconnect address: " + theNode);
        }
        if (!ProUtil.isSwitchData(controller)) {
            logInfo("Using server: " + theNode.getNick() + ", ID: " + theNodeId + " @ "
                    + theNode.getReconnectAddress());
        }
        init(theNode, allowServerChange);
    }

    private void init(Member serverNode, boolean serverChange) {
        Reject.ifNull(serverNode, "Server node is null");
        boolean firstCall = listenerSupport == null;
        if (firstCall) {
            listenerSupport = ListenerSupportFactory.createListenerSupport(ServerClientListener.class);
        }
        setNewServerNode(serverNode);
        // Allowed by default
        allowServerChange = serverChange;
        setAnonAccount();

        if (firstCall) {
            getController().getNodeManager().addNodeManagerListener(new MyNodeManagerListener());
            getController().getFolderRepository().addFolderRepositoryListener(new MyFolderRepositoryListener());
        }
    }

    private boolean isRememberPassword() {
        return PreferencesEntry.SERVER_REMEMBER_PASSWORD.getValueBoolean(getController());
    }

    // Basics *****************************************************************

    public void start() {
        boolean allowLAN2Internet = ConfigurationEntry.SERVER_CONNECT_FROM_LAN_TO_INTERNET
                .getValueBoolean(getController());
        if (!allowLAN2Internet && getController().isLanOnly() && !server.isOnLAN()) {
            logWarning("Not connecting to server: " + server + ". Reason: Server not on LAN");
        }
        getController().scheduleAndRepeat(new ServerConnectTask(), 3L * 1000L, 1000L * 20);
        getController().scheduleAndRepeat(new AutoLoginTask(), 10L * 1000L, 1000L * 30);
        // Wait 10 seconds at start
        getController().scheduleAndRepeat(new HostingServersConnector(), 10L * 1000L,
                1000L * Constants.HOSTING_FOLDERS_REQUEST_INTERVAL);
        // Don't start, not really required?
        // getController().scheduleAndRepeat(new AccountRefresh(), 1000L * 30,
        // 1000L * 30);
    }

    /**
     * Answers if the node is a temporary node info for a server. It does not
     * contains a valid id, but a hostname/port.
     *
     * @param node
     * @return true if the node is a temporary node info.
     */
    public static boolean isTempServerNode(Member node) {
        return node.getId().startsWith(MEMBER_ID_TEMP_PREFIX);
    }

    /**
     * Answers if the node is a temporary node info for a server. It does not
     * contains a valid id, but a hostname/port.
     * 
     * @param node
     * @return true if the node is a temporary node info.
     */
    public static boolean isTempServerNode(MemberInfo node) {
        return node.id.startsWith(MEMBER_ID_TEMP_PREFIX);
    }

    /**
     * @return true if the set server is part of the public PowerFolder cloud
     *         (my.powerfolder.com). false if custom own inhouse server host is
     *         set or not set at all (non inhouse server).
     */
    public boolean isPowerFolderCloud() {
        return isPowerFolderCloud(getController());
    }

    /**
     * @return true if the set server is part of the public PowerFolder cloud
     *         (my.powerfolder.com). false if custom own inhouse server host is
     *         set or not set at all (non inhouse server).
     */
    public static boolean isPowerFolderCloud(Controller contoller) {
        String nodeId = ConfigurationEntry.SERVER_NODEID.getValue(contoller);
        String host = ConfigurationEntry.SERVER_HOST.getValue(contoller);
        return StringUtils.isNotBlank(nodeId) && nodeId.toUpperCase().contains("WEBSERVICE")
                && StringUtils.isNotBlank(host) && host.toLowerCase().contains("powerfolder.com");
    }

    /**
     * @return the server to connect to.
     */
    public Member getServer() {
        return server;
    }

    /**
     * @param conHan
     * @return true if the node is the primary login server for the current
     *         account. account.
     */
    public boolean isPrimaryServer(ConnectionHandler conHan) {
        if (server.getInfo().equals(conHan.getIdentity().getMemberInfo())) {
            return true;
        }
        if (isTempServerNode(server)) {
            if (server.getReconnectAddress().equals(conHan.getRemoteAddress())) {
                return true;
            }
            // Try check by hostname / port
            InetSocketAddress nodeSockAddr = conHan.getRemoteAddress();
            InetSocketAddress serverSockAddr = server.getReconnectAddress();
            if (nodeSockAddr == null || serverSockAddr == null) {
                return false;
            }
            InetAddress nodeAddr = nodeSockAddr.getAddress();
            InetAddress serverAddr = serverSockAddr.getAddress();
            if (nodeAddr == null || serverAddr == null) {
                return false;
            }
            String nodeHost = NetworkUtil.getHostAddressNoResolve(nodeAddr);
            String serverHost = NetworkUtil.getHostAddressNoResolve(serverAddr);
            int nodePort = nodeSockAddr.getPort();
            int serverPort = serverSockAddr.getPort();
            return nodeHost.equalsIgnoreCase(serverHost) && nodePort == serverPort;
        }
        return false;
    }

    /**
     * @param node
     * @return true if the node is the primary login server for the current
     *         account. account.
     */
    public boolean isPrimaryServer(Member node) {
        if (server.equals(node)) {
            return true;
        }
        if (isTempServerNode(server)) {
            if (server.getReconnectAddress().equals(node.getReconnectAddress())) {
                return true;
            }
            // Try check by hostname / port
            InetSocketAddress nodeSockAddr = node.getReconnectAddress();
            InetSocketAddress serverSockAddr = server.getReconnectAddress();
            if (nodeSockAddr == null || serverSockAddr == null) {
                return false;
            }
            InetAddress nodeAddr = nodeSockAddr.getAddress();
            InetAddress serverAddr = serverSockAddr.getAddress();
            if (nodeAddr == null || serverAddr == null) {
                return false;
            }
            String nodeHost = NetworkUtil.getHostAddressNoResolve(nodeAddr);
            String serverHost = NetworkUtil.getHostAddressNoResolve(serverAddr);
            int nodePort = nodeSockAddr.getPort();
            int serverPort = serverSockAddr.getPort();
            return nodeHost.equalsIgnoreCase(serverHost) && nodePort == serverPort;
        }
        return false;
    }

    /**
     * @param node
     * @return true if the node is a part of the server cloud.
     */
    public boolean isClusterServer(Member node) {
        return node.isServer() || isPrimaryServer(node);
    }

    /**
     * @return all KNOWN servers of the cluster
     */
    public Collection<Member> getServersInCluster() {
        List<Member> servers = new LinkedList<Member>();
        for (Member node : getController().getNodeManager().getNodesAsCollection()) {
            if (node.isServer()) {
                servers.add(node);
            }
        }
        // Every day I'm shuffleing
        Collections.shuffle(servers);
        return servers;
    }

    /**
     * Sets/Changes the server.
     *
     * @param serverNode
     * @param allowServerChange
     */
    public void setServer(Member serverNode, boolean allowServerChange) {
        Reject.ifNull(serverNode, "Server node is null");
        setNewServerNode(serverNode);
        this.allowServerChange = allowServerChange;
        if (StringUtils.isBlank(username) || StringUtils.isBlank(passwordObf)) {
            loginWithLastKnown();
        } else {
            login(username, passwordObf, true);
        }
        if (!isConnected()) {
            server.markForImmediateConnect();
        }
    }

    /**
     * @return if the server is connected
     */
    public boolean isConnected() {
        return server.isMySelf() || server.isConnected();
    }

    /**
     * @return the URL of the web access to the server (cluster).
     */
    public String getWebURL() {
        String webURL = Util.removeLastSlashFromURI(ConfigurationEntry.SERVER_WEB_URL.getValue(getController()));
        if (!StringUtils.isBlank(webURL)) {
            return webURL;
        }
        if (accountDetails != null && accountDetails.getAccount() != null
                && accountDetails.getAccount().getServer() != null
                && !StringUtils.isBlank(accountDetails.getAccount().getServer().getWebUrl())) {
            return accountDetails.getAccount().getServer().getWebUrl();
        }
        // No web url.
        return null;
    }

    public String getWebURL(String uri, boolean withCredentials) {
        if (!hasWebURL()) {
            return null;
        }
        String webURL = getWebURL();
        if (StringUtils.isBlank(uri)) {
            uri = "";
        }
        if (uri.startsWith("/")) {
            uri = uri.substring(1);
        }
        if (!withCredentials || !ConfigurationEntry.WEB_PASSWORD_ALLOWED.getValueBoolean(getController())) {
            return webURL + '/' + uri;
        }
        String fullURL = getLoginURLWithCredentials();
        try {
            if (fullURL.contains("?")) {
                fullURL += "&";
            } else {
                fullURL += "?";
            }
            fullURL += Constants.LOGIN_PARAM_ORIGINAL_URI + "=" + URLEncoder.encode(uri, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        return fullURL;
    }

    /**
     * @return true if the connected server offers a web interface.
     */
    public boolean hasWebURL() {
        return getWebURL() != null;
    }

    /**
     * #2488
     *
     * @return true if web DAV is available at the server.
     */
    public boolean supportsWebDAV() {
        if (!hasWebURL()) {
            return false;
        }
        return ConfigurationEntry.WEB_DAV_ENABLED.getValueBoolean(getController());
    }

    /**
     * #2488
     *
     * @return true if web login as regular user is allowed at the server.
     */
    public boolean supportsWebLogin() {
        if (!hasWebURL()) {
            return false;
        }
        if (ConfigurationEntry.WEB_LOGIN_ALLOWED.getValueBoolean(getController())) {
            return true;
        }
        if (accountDetails == null) {
            return false;
        }
        return accountDetails.getAccount().hasPermission(AdminPermission.INSTANCE);
    }

    /**
     * Convenience method for getting login URL with preset username and
     * password if possible
     *
     * @return the login URL
     */
    public String getLoginURLWithCredentials() {
        if (!hasWebURL()) {
            return null;
        }

        // PFS-862: Start
        if (isLoggedIn()) {
            try {
                String otp = getSecurityService().requestOTP();
                if (isFine()) {
                    logFine("Retrieved OTP for " + accountDetails.getAccount().getUsername() + ": " + otp);
                }
                if (LoginUtil.isOTPValid(otp)) {
                    return LoginUtil.decorateURL(getWebURL(Constants.LOGIN_URI, false), null, otp);
                }
            } catch (Exception e) {
                // Not supported. Maybe old server version. Ignore
                logFine("Unable to generate OTP. " + e);
            }
        }
        // PFS-862: End

        if (!ConfigurationEntry.WEB_PASSWORD_ALLOWED.getValueBoolean(getController())) {
            return getWebURL();
        }
        return LoginUtil.decorateURL(getWebURL(Constants.LOGIN_URI, false), username, passwordObf);
    }

    /**
     * @param foInfo
     * @return the direct URL to the folder
     */
    public String getFolderURL(FolderInfo foInfo) {
        if (!hasWebURL()) {
            return null;
        }
        return getWebURL("/files/" + Base64.encode4URL(foInfo.id), false);
    }

    /**
     * @param foInfo
     * @return the direct URL to the folder including login if necessary
     */
    public String getFolderURLWithCredentials(FolderInfo foInfo) {
        if (!supportsWebLogin()) {
            return null;
        }
        String folderURI = getFolderURL(foInfo);
        folderURI = folderURI.replace(getWebURL(), "");
        String loginURL = getLoginURLWithCredentials();
        if (loginURL.contains("?")) {
            loginURL += "&";
        } else {
            loginURL += "?";
        }
        loginURL += Constants.LOGIN_PARAM_ORIGINAL_URI;
        loginURL += "=";
        loginURL += folderURI;
        return loginURL;
    }

    /**
     * #2675: Shell integration.
     *
     * @param fInfo
     * @return
     */
    public String getFileLinkURL(FileInfo fInfo) {
        Reject.ifNull(fInfo, "fileinfo");
        if (!hasWebURL()) {
            return null;
        }
        return getWebURL(Constants.GET_LINK_URI + '/' + Base64.encode4URL(fInfo.getFolderInfo().getId()) + '/'
                + Util.endcodeForURL(fInfo.getRelativeName()), true);
    }

    /**
     * Generate a URL that directs to a web colaboration tool.
     * 
     * @param fInfo
     *            The file to open
     * @return The URL
     */
    public String getOpenURL(FileInfo fInfo) {
        Reject.ifNull(fInfo, "fileInfo");
        if (!hasWebURL()) {
            return null;
        }
        return getWebURL(Constants.OPEN_LINK_URI + '/' + Base64.encode4URL(fInfo.getFolderInfo().getId()) + '/'
                + Util.endcodeForURL(fInfo.getRelativeName()), true);
    }

    /**
     * @return if password recovery is supported
     */
    public boolean supportsRecoverPassword() {
        return StringUtils.isNotBlank(getRecoverPasswordURL());
    }

    /**
     * Convenience method for getting login URL with preset username if possible
     *
     * @return the registration URL for this server.
     */
    public String getRecoverPasswordURL() {
        if (!hasWebURL()) {
            return null;
        }
        if (!ConfigurationEntry.SERVER_RECOVER_PASSWORD_ENABLED.getValueBoolean(getController())) {
            return null;
        }
        String url = getWebURL(Constants.LOGIN_URI, false);
        if (StringUtils.isNotBlank(username)) {
            url = LoginUtil.decorateURL(url, username, (char[]) null);
        }
        return url;
    }

    /**
     * @return true if client supports register on registerURL.
     */
    public boolean supportsWebRegistration() {
        return ConfigurationEntry.SERVER_REGISTER_ENABLED.getValueBoolean(getController());
    }

    /**
     * Convenience method for getting register URL
     * 
     * @return the registration URL for this server.
     */
    public String getRegisterURL() {
        if (!supportsWebRegistration()) {
            return null;
        }
        if (!hasWebURL()) {
            return null;
        }
        return getWebURL(Constants.REGISTER_URI, false);
    }

    /**
     * Convenience method for getting register URL
     * 
     * @return the registration URL for this server.
     */
    public String getRegisterURLReferral() {
        String url = getRegisterURL();
        if (StringUtils.isBlank(url)) {
            return getWebURL();
        }
        if (!isLoggedIn()) {
            return url;
        }
        try {
            if (url.contains("?")) {
                url += "&";
            } else {
                url += "?";
            }
            return url + "ref=" + URLEncoder.encode(getAccount().getOID(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }

    /**
     * Convenience method for getting activation URL
     *
     * @return the activation URL for this server.
     */
    public String getActivationURL() {
        if (!hasWebURL()) {
            return null;
        }
        return getWebURL(Constants.ACTIVATE_URI, false);
    }

    /**
     * Convenience method to get the URL to an avatar
     *
     * @param information about the account
     * @return the avatar URL.
     */
    public String getAvatarURL(AccountInfo aInfo, boolean thumbnail) {
        if (!hasWebURL()) {
            return null;
        }
        StringBuilder url = new StringBuilder();
        url.append("/avatars/user/");
        url.append(aInfo.getOID());
        if (thumbnail) {
            url.append("_tn");
        }
        return getWebURL(url.toString(), false);
    }

    /**
     * @return if all new folders should be backed up by the server/cloud.
     */
    public boolean isBackupByDefault() {
        return PreferencesEntry.USE_ONLINE_STORAGE.getValueBoolean(getController())
                || getController().isBackupOnly() || !PreferencesEntry.EXPERT_MODE.getValueBoolean(getController());
    }

    // Login ******************************************************************

    /**
     * @return true if we know last login data. uses default account setting as
     *         fallback
     */
    public boolean isLastLoginKnown() {
        return ConfigurationEntry.SERVER_CONNECT_USERNAME.hasValue(getController());
    }

    /**
     * Tries to logs in with the last know username/password combination for
     * this server.uses default account setting as fallback
     *
     * @return the identity with this username or <code>InvalidAccount</code> if
     *         login failed.
     */
    public Account loginWithLastKnown() {

        String un = null;
        char[] pw = null;

        if (ConfigurationEntry.SERVER_CONNECT_USERNAME.hasValue(getController())) {
            un = ConfigurationEntry.SERVER_CONNECT_USERNAME.getValue(getController());
            pw = LoginUtil.deobfuscate(ConfigurationEntry.SERVER_CONNECT_PASSWORD.getValue(getController()));

            if (pw == null) {
                String pws = ConfigurationEntry.SERVER_CONNECT_PASSWORD_CLEAR.getValue(getController());
                if (StringUtils.isNotBlank(pws)) {
                    pw = Util.toCharArray(pws);
                }
            }
        }

        if (StringUtils.isNotBlank(getController().getCLIUsername())) {
            un = getController().getCLIUsername();
        }
        if (StringUtils.isNotBlank(getController().getCLIPassword())) {
            pw = Util.toCharArray(getController().getCLIPassword());
        }

        if (ConfigurationEntry.SERVER_CONNECT_NO_PASSWORD_ALLOWED.getValueBoolean(getController())) {
            if (StringUtils.isBlank(un)) {
                un = System.getProperty("user.name");
            }
            if (pw == null || pw.length == 0) {
                pw = Util.toCharArray(ProUtil.rtrvePwssd(getController(), un));
            }
        }

        String systemUserName = System.getProperty("user.name");
        // PFC-2533: Don't use it for CFN
        // if (StringUtils.isBlank(un)
        // && LoginUtil.isValidUsername(getController(), systemUserName))
        // {
        // un = systemUserName;
        // }

        if (StringUtils.isBlank(un) && (pw == null || pw.length == 0)
                && ConfigurationEntry.KERBEROS_SSO_ENABLED.getValueBoolean(getController())) {
            un = systemUserName;
        }

        if (StringUtils.isBlank(un)) {
            logFine("Not logging in. Username blank");
        } else {
            if (!ProUtil.isSwitchData(getController())) {
                logInfo("Logging into server " + getServerString() + ". Username: " + un);
            }
            return login(un, pw);
        }
        // Failed!
        return null;
    }

    /**
     * Log out of online storage.
     */
    public void logout() {
        username = null;
        passwordObf = null;
        try {
            securityService.logout();
        } catch (Exception e) {
            logWarning("Unable to logout. " + e);
        }
        saveLastKnowLogin(null, null);
        setAnonAccount();
        fireLogin(accountDetails);
    }

    /**
     * Logs into the server and saves the identity as my login.
     * <p>
     * If the server is not connected and invalid account is returned and the
     * login data saved for auto-login on reconnect.
     *
     * @param theUsername
     * @param thePassword
     * @return the identity with this username or <code>InvalidAccount</code> if
     *         login failed. NEVER returns <code>null</code>
     */
    public Account login(String theUsername, char[] thePassword) {
        return login(theUsername, LoginUtil.obfuscate(thePassword), true);
    }

    /**
     * Logs into the server and saves the identity as my login.
     * <p>
     * If the server is not connected and invalid account is returned and the
     * login data saved for auto-login on reconnect.
     *
     * @param theUsername
     * @param thePasswordObj
     *            the obfuscated password
     * @param saveLastLogin
     *            if the last login should be remembered.
     * @return the identity with this username or <code>InvalidAccount</code> if
     *         login failed. NEVER returns <code>null</code>
     */
    public Account login(String theUsername, String thePasswordObj, boolean saveLastLogin) {
        logFine("Login with: " + theUsername);
        synchronized (loginLock) {
            loggingIn.set(true);
            String prevUsername = username;
            String prevPasswordObf = passwordObf;
            try {
                username = theUsername;
                passwordObf = thePasswordObj;
                if (saveLastLogin) {
                    saveLastKnowLogin(username, passwordObf);
                }
                boolean disconnected = !server.isConnected();
                boolean pwEmpty = StringUtils.isBlank(passwordObf);
                boolean noKerberosLogin = !isKerberosLogin();
                if (disconnected || (pwEmpty && noKerberosLogin)) {
                    setAnonAccount();
                    fireLogin(accountDetails);
                    return accountDetails.getAccount();
                }
                boolean loginOk = false;
                char[] pw = LoginUtil.deobfuscate(passwordObf);
                try {
                    if (isShibbolethLogin()) {
                        // PFC-2534: Start
                        try {
                            String currentIdP = ConfigurationEntry.SERVER_IDP_LAST_CONNECTED_ECP
                                    .getValue(getController());
                            boolean idpEqual = StringUtils.isEqual(lastIdPUsed, currentIdP);
                            boolean pwEqual = StringUtils.isEqual(prevPasswordObf, passwordObf);
                            boolean unEqual = StringUtils.isEqual(prevUsername, username);
                            if (shibbolethUnauthRetriesSkip != 0 && unEqual && pwEqual && idpEqual) {
                                shibbolethUnauthRetriesSkip--;
                                if (isFine()) {
                                    logFine("Skipping login another " + shibbolethUnauthRetriesSkip + " times");
                                }
                                setAnonAccount();
                                return accountDetails.getAccount();
                            }

                            lastIdPUsed = currentIdP;
                            shibbolethUnauthRetriesSkip = 0;
                        } catch (RuntimeException e) {
                            logWarning("An error occured skipping shibboleth login: " + e);
                        }
                        // PFC-2534: End

                        boolean externalUser = prepareShibbolethLogin(username, pw,
                                (prevUsername != null && !prevUsername.equals(username))
                                        || (prevPasswordObf != null && !prevPasswordObf.equals(passwordObf)));
                        if (externalUser) {
                            loginOk = securityService.login(username, pw);
                        } else if (shibUsername != null && shibToken != null) {
                            loginOk = securityService.login(shibUsername, Util.toCharArray(shibToken));
                        } else {
                            logWarning("Neither Shibboleth nor external login possible!");
                        }
                    } else if (isKerberosLogin()) {
                        byte[] serviceTicket = prepareKerberosLogin();
                        loginOk = securityService.login(username, serviceTicket);
                    } else {
                        loginOk = securityService.login(username, pw);
                    }

                    lastLoginSuccessful.set(loginOk);

                    loginExecuted.set(true);
                } catch (RemoteCallException e) {
                    if (e.getCause() instanceof NoSuchMethodException) {
                        // Old server version (Pre 1.5.0 or older)
                        // Try it the old fashioned way
                        logSevere("Client incompatible with server: Server version too old");
                    }
                    // Rethrow
                    throw e;
                } finally {
                    LoginUtil.clear(pw);
                }

                if (!loginOk) {
                    logWarning("Login to server " + server + " (user " + theUsername + ") failed!");
                    setAnonAccount();
                    fireLogin(accountDetails, false);
                    return accountDetails.getAccount();
                }
                AccountDetails newAccountDetails = securityService.getAccountDetails();
                logInfo("Login to server " + server.getReconnectAddress() + " (user " + theUsername + ") result: "
                        + newAccountDetails);
                if (newAccountDetails != null) {
                    accountDetails = newAccountDetails;

                    if (updateConfig) {
                        boolean configChanged;
                        if (accountDetails.getAccount().getServer() != null) {
                            configChanged = setServerWebURLInConfig(
                                    accountDetails.getAccount().getServer().getWebUrl());
                            configChanged = setServerHTTPTunnelURLInConfig(
                                    accountDetails.getAccount().getServer().getHTTPTunnelUrl()) || configChanged;
                        } else {
                            configChanged = setServerWebURLInConfig(null);
                            configChanged = setServerHTTPTunnelURLInConfig(null) || configChanged;
                        }
                        if (configChanged) {
                            getController().saveConfig();
                        }
                    }

                    // Fire login success
                    loggingIn.set(false);
                    fireLogin(accountDetails);
                    getController().schedule(new Runnable() {
                        @Override
                        public void run() {
                            // Also switches server
                            updateLocalSettings(accountDetails.getAccount());
                        }
                    }, 0);
                } else {
                    setAnonAccount();
                    fireLogin(accountDetails, false);
                }

                return accountDetails.getAccount();
            } catch (Exception e) {
                logWarning("Unable to login: " + e);
                if (isShibbolethLogin()) {
                    // PFC-2534: Start
                    //                    username = prevUsername;
                    //                    passwordObf = prevPasswordObf;
                    // PFC-2534: End
                    saveLastKnowLogin(username, passwordObf);
                }
                setAnonAccount();
                fireLogin(accountDetails, false);
                return accountDetails.getAccount();
            } finally {
                loggingIn.set(false);
            }
        }
    }

    private boolean isKerberosLogin() {
        return ConfigurationEntry.KERBEROS_SSO_ENABLED.getValueBoolean(getController())
                && StringUtils.isBlank(passwordObf);
    }

    private byte[] prepareKerberosLogin() {
        try {
            Path outputFile = Controller.getTempFilesLocation().resolve("login.conf");

            if (Files.notExists(outputFile)) {
                InputStream configFile = Thread.currentThread().getContextClassLoader()
                        .getResourceAsStream("kerberos/login.conf");
                PathUtils.copyFromStreamToFile(configFile, outputFile);
            }

            System.setProperty("java.security.auth.login.config", outputFile.toAbsolutePath().toString());

            System.setProperty("java.security.krb5.realm",
                    ConfigurationEntry.KERBEROS_SSO_REALM.getValue(getController()));
            String kdc = ConfigurationEntry.KERBEROS_SSO_KDC.getValue(getController());
            System.setProperty("java.security.krb5.kdc", kdc);

            LoginContext lc = new LoginContext("SignedOnUserLoginContext", new TextCallbackHandler());
            lc.login();
            Subject clientSubject = lc.getSubject();

            username = clientSubject.getPrincipals().iterator().next().getName();
            return Subject.doAs(clientSubject, new ServiceTicketGenerator());
        } catch (Exception e) {
            logWarning("Unable to login: " + e);
            return null;
        } finally {
            loggingIn.set(false);
        }
    }

    private boolean isShibbolethLogin() {
        return ConfigurationEntry.SERVER_IDP_DISCO_FEED_URL.hasValue(getController());
    }

    public boolean isAllowedToCreateFolders() {
        if (getAccount().getOSSubscription().getStorageSize() <= 0) {
            return false;
        }
        if (ConfigurationEntry.SECURITY_PERMISSIONS_STRICT.getValueBoolean(getController())
                && !getAccount().hasPermission(FolderCreatePermission.INSTANCE)) {
            return false;
        }
        return true;
    }

    /**
     * Prepare login information for shibboleth environment.
     *
     * @param username
     * @param thePassword
     * @param userChanged
     * @return True if the user should login as external user, false if
     *         shibboleth is used.
     */
    private boolean prepareShibbolethLogin(String username, char[] thePassword, boolean userChanged) {
        String idpURLString = ConfigurationEntry.SERVER_IDP_LAST_CONNECTED_ECP.getValue(getController());

        if (StringUtils.isBlank(idpURLString)) {
            shibUsername = null;
            shibToken = null;
            throw new SecurityException("Your organization is unreachable");
        } else if (userChanged) {
            shibUsername = null;
            shibToken = null;
        } else if ("ext".equals(idpURLString)) {
            return true;
        }

        boolean tokenIsValid = false;
        try {
            tokenIsValid = shibToken != null && shibToken.contains(":") && System.currentTimeMillis() <= Long
                    .valueOf(shibToken.substring(shibToken.indexOf(':') + 1, shibToken.length()));
        } catch (Exception e) {
            logFine("Unusable Shibboleth Token: " + shibToken + " valid ? " + tokenIsValid);
            shibUsername = null;
            shibToken = null;
        }
        if (StringUtils.isBlank(shibUsername) || StringUtils.isBlank(shibToken) || !tokenIsValid) {
            String spURL = getWebURL(
                    Constants.LOGIN_SHIBBOLETH_CLIENT_URI + '/' + getController().getMySelf().getId(), false);
            URI spURI;
            try {
                spURI = new URI(spURL);
            } catch (URISyntaxException e) {
                shibUsername = null;
                shibToken = null;
                // Should not happen
                throw new RuntimeException("Unable to resolve service provider URL: " + spURL + ". " + e);
            }

            URI idpURI = null;
            try {
                idpURI = new URI(idpURLString);
            } catch (Exception e) {
                shibUsername = null;
                shibToken = null;
                // Should not happen
                throw new RuntimeException("Unable to resolve identity provider URL: "
                        + ConfigurationEntry.SERVER_IDP_LAST_CONNECTED_ECP.getValue(getController()) + ". " + e);
            }

            // PFC-2496: Start
            DefaultHttpClient dhc = new DefaultHttpClient();

            // Set Proxy credentials, if configured
            if (ConfigurationEntry.HTTP_PROXY_HOST.hasValue(getController())) {
                String proxyUsername = "";
                String proxyPassword = "";

                if (ConfigurationEntry.HTTP_PROXY_USERNAME.hasValue(getController())) {
                    proxyUsername = ConfigurationEntry.HTTP_PROXY_USERNAME.getValue(getController());

                    if (ConfigurationEntry.HTTP_PROXY_PASSWORD.hasValue(getController())) {
                        proxyPassword = ConfigurationEntry.HTTP_PROXY_PASSWORD.getValue(getController());
                    }
                }

                dhc.getCredentialsProvider().setCredentials(
                        new AuthScope(ConfigurationEntry.HTTP_PROXY_HOST.getValue(getController()),
                                ConfigurationEntry.HTTP_PROXY_PORT.getValueInt(getController())),
                        new UsernamePasswordCredentials(proxyUsername, proxyPassword));

                HttpHost proxy = new HttpHost(ConfigurationEntry.HTTP_PROXY_HOST.getValue(getController()),
                        ConfigurationEntry.HTTP_PROXY_PORT.getValueInt(getController()));
                dhc.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
            }
            // PFC-2496: End

            ECPAuthenticator auth = new ECPAuthenticator(dhc, username, new String(thePassword), idpURI, spURI);
            String[] result;
            try {
                result = auth.authenticate();
                shibUsername = result[0];
                shibToken = result[1];
            } catch (ECPUnauthorizedException e) {
                shibbolethUnauthRetriesSkip = ConfigurationEntry.SERVER_LOGIN_SKIP_RETRY
                        .getValueInt(getController());
                shibUsername = null;
                shibToken = null;
                throw new SecurityException(e);
            } catch (ECPAuthenticationException e) {
                shibUsername = null;
                shibToken = null;
                throw new SecurityException(e);
            }
        }

        return false;
    }

    private void findAlternativeServer() {
        if (!allowServerChange) {
            return;
        }
        if (getController().getMySelf().isServer()) {
            // Don't
            return;
        }
        if (getController().isShuttingDown() || !getController().isStarted()) {
            return;
        }
        if (isFine()) {
            logFine("findAlternativeServer: " + getServersInCluster());
        }
        for (Member server : getServersInCluster()) {
            if (!server.isConnected()) {
                server.markForImmediateConnect();
            }
            Waiter w = new Waiter(500);
            while (w.isTimeout() && !server.isConnected()) {
                w.waitABit();
            }
            if (server.isConnected()) {
                if (!server.equals(this.server)) {
                    logInfo("Switching to new server: " + server);
                    try {
                        setServer(server, allowServerChange);
                        break;
                    } catch (Exception e) {
                        logWarning("Unable to switch server to " + server.getNick() + ". Searching for new..." + e);
                    }
                }
            }
        }
    }

    /**
     * Load a new configuration from URL configURL
     *
     * @param configURL
     */
    public void loadConfigURL(String configURL) {
        Reject.ifBlank(configURL, "configURL");
        try {
            // load the configuration from the url ...
            Properties props = ConfigurationLoader.loadPreConfiguration(configURL.trim());

            ConfigurationLoader.merge(props, getController());
            String networkID = (String) props.get(ConfigurationEntry.NETWORK_ID.getConfigKey());
            String name = (String) props.get(ConfigurationEntry.SERVER_NAME.getConfigKey());
            String host = (String) props.get(ConfigurationEntry.SERVER_HOST.getConfigKey());
            String nodeId = (String) props.get(ConfigurationEntry.SERVER_NODEID.getConfigKey());
            String tunnelURL = (String) props.get(ConfigurationEntry.SERVER_HTTP_TUNNEL_RPC_URL.getConfigKey());
            String webURL = (String) props.get(ConfigurationEntry.SERVER_WEB_URL.getConfigKey());

            logInfo("Loaded " + props.size() + " from " + configURL + " network ID: " + networkID);
            if (StringUtils.isBlank(host)) {
                throw new IOException("Hostname not found");
            }

            String oldNetworkID = getController().getMySelf().getInfo().networkId;
            if (StringUtils.isNotBlank(networkID)) {
                getController().getMySelf().getInfo().networkId = networkID;
            } else {
                getController().getMySelf().getInfo().networkId = ConfigurationEntry.NETWORK_ID.getDefaultValue();
            }
            String newNetworkID = getController().getMySelf().getInfo().networkId;
            boolean networkIDChanged = !Util.equals(oldNetworkID, newNetworkID);
            if (networkIDChanged) {
                getController().getNodeManager().shutdown();
            }

            init(getController(), name, host, nodeId, allowServerChange, updateConfig);

            // Store in config
            setServerWebURLInConfig(webURL);
            setServerHTTPTunnelURLInConfig(tunnelURL);
            setServerInConfig(getServer().getInfo());
            ConfigurationEntry.NETWORK_ID.setValue(getController(), newNetworkID);
            ConfigurationEntry.CONFIG_URL.setValue(getController(), configURL);

            getController().saveConfig();

            if (networkIDChanged) {
                // Restart nodemanager
                getController().getNodeManager().start();
            }

            connectHostingServers();
        } catch (Exception e) {
            logWarning("Could not load connection infos from " + configURL + ": " + e.getMessage());
        }
    }

    /**
     * Are we currently logging in?
     *
     * @return
     */
    public boolean isLoggingIn() {
        return loggingIn.get();
    }

    /**
     * Blocks until the current login attempt has finished.
     */
    public void waitForLoginComplete() {
        synchronized (loginLock) {
        }
    }

    /**
     * @return true if the last attempt to login to the online storage was ok.
     *         false if not or no login tried yet.
     */
    public boolean isLoggedIn() {
        return getAccount() != null && getAccount().isValid();
    }

    /**
     * @return true if once a login call to the server was successfully executed.
     */
    public boolean isLoginExecuted() {
        return loginExecuted.get();
    }

    /**
     * @return the username that is set for login.
     */
    public String getUsername() {
        return username;
    }

    /**
     * @return true if the currently set password is empty.
     */
    public boolean isPasswordEmpty() {
        return StringUtils.isBlank(passwordObf);
    }

    /**
     *
     * @return true if the password is empty and Single Sign-on via Kerberos is disabled.
     */
    public boolean isPasswordRequired() {
        return StringUtils.isBlank(passwordObf)
                && !ConfigurationEntry.KERBEROS_SSO_ENABLED.getValueBoolean(getController());
    }

    /**
     * ATTENTION: Make sure the returned char array is purged/cleared as soon as
     * possible with {@link LoginUtil#clear(char[])}
     *
     * @return the password that is set for login.
     */
    public char[] getPassword() {
        return LoginUtil.deobfuscate(passwordObf);
    }

    /**
     * ATTENTION: This password must not be used for long. It cannot be
     * purged/cleared from memory.
     *
     * @return the password used in CLEAR TEXT.
     */
    public String getPasswordClearText() {
        char[] pw = LoginUtil.deobfuscate(passwordObf);
        String txt = Util.toString(pw);
        LoginUtil.clear(pw);
        return txt;
    }

    /**
     * @return the {@link AccountInfo} for the logged in account. or null if not
     *         logged in.
     */
    public AccountInfo getAccountInfo() {
        Account a = getAccount();
        return a != null && a.isValid() ? a.createInfo() : null;
    }

    /**
     * @return the user/account of the last login.
     */
    public Account getAccount() {
        return accountDetails != null ? accountDetails.getAccount() : null;
    }

    public AccountDetails getAccountDetails() {
        return accountDetails;
    }

    /**
     * Re-loads the account details from server. Should be done if it's likely
     * that currently logged in account has changed.
     *
     * @return the new account details
     */
    public AccountDetails refreshAccountDetails() {
        AccountDetails newDetails = securityService.getAccountDetails();
        if (newDetails != null) {
            accountDetails = newDetails;
            fireAccountUpdates(accountDetails);
            updateLocalSettings(accountDetails.getAccount());
        } else {
            setAnonAccount();
            fireLogin(accountDetails, false);
        }
        if (isFine()) {
            logFine("Refreshed " + accountDetails);
        }
        return accountDetails;
    }

    private void updateLocalSettings(Account a) {
        updateServer(a);
        updateFriendsList(a);
        getController().getFolderRepository().updateFolders(a);
        scheduleConnectHostingServers();
    }

    private void updateServer(Account a) {
        // Possible switch to new server
        final ServerInfo targetServer = a.getServer();
        if (targetServer == null || !allowServerChange) {
            return;
        }
        // Not hosted on the server we just have logged into.
        boolean changeServer = !server.getInfo().equals(targetServer.getNode());
        if (!changeServer) {
            return;
        }
        final Member targetServerNode = targetServer.getNode().getNode(getController(), true);
        boolean checked = currentlyHammeringServers() || !targetServerNode.isConnected();
        if (!checked) {
            logInfo("Switching from " + server.getNick() + " to " + targetServerNode.getNick());
            changeToServer(targetServer);
        } else {
            if (currentlyHammeringServers()) {
                logInfo("Switching from " + server.getNick() + " to " + targetServerNode.getNick() + " in "
                        + HAMMER_DELAY / 1000 + "s");
            } else {
                logInfo("Switching from " + server.getNick() + " to " + targetServerNode.getNick()
                        + " after connect");
            }

            Waiter w = new Waiter(HAMMER_DELAY);
            while (!w.isTimeout()) {
                w.waitABit();
                if (!currentlyHammeringServers() && targetServerNode.isConnected()) {
                    break;
                }
            }
        }

        if (checked) {
            getController().getIOProvider().startIO(new Runnable() {
                @Override
                public void run() {
                    if (!targetServerNode.isConnected()) {
                        if (!isConnected()) {
                            logWarning("Unable to connect to server: " + targetServerNode.getNick()
                                    + ". Searching for alternatives...");
                            findAlternativeServer();
                        }
                    } else {
                        boolean changeServer = !server.getInfo().equals(targetServer.getNode());
                        if (changeServer) {
                            changeToServer(targetServer);
                        }
                    }
                }
            });
        }
    }

    private void updateFriendsList(Account a) {
        for (MemberInfo nodeInfo : a.getComputers()) {
            Member node = nodeInfo.getNode(getController(), true);
            if (!node.isFriend()) {
                node.setFriend(true, null);
            }
        }
    }

    // Services ***************************************************************

    public <T> T getService(Class<T> serviceInterface) {
        return RemoteServiceStubFactory.createRemoteStub(getController(), serviceInterface, server,
                throwableHandler);
    }

    public SecurityService getSecurityService() {
        return securityService;
    }

    public AccountService getAccountService() {
        return userService;
    }

    public FolderService getFolderService() {
        return folderService;
    }

    // Conviniece *************************************************************

    /**
     * @return the joined folders by the Server.
     */
    public List<Folder> getJoinedCloudFolders() {
        List<Folder> mirroredFolders = new ArrayList<Folder>();
        for (Folder folder : getController().getFolderRepository().getFolders()) {
            if (joinedByCloud(folder)) {
                mirroredFolders.add(folder);
            }
        }
        return mirroredFolders;
    }

    /**
     * @return a list of folder infos that are available on this account. These
     *         folder may or may not be backed up by the Online Storage/Server.
     */
    public Collection<FolderInfo> getAccountFolders() {
        return getAccount().getFolders();
    }

    /**
     * @param folder
     *            the folder to check.
     * @return true if the cloud has joined the folder.
     */
    public boolean joinedByCloud(Folder folder) {
        if (folder.hasMember(server)) {
            return true;
        }
        for (Member member : folder.getMembersAsCollection()) {
            if (member.isServer() && !member.isMySelf()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param foInfo
     *            the folder to check.
     * @return true if the cloud has joined the folder.
     */
    public boolean joinedByCloud(FolderInfo foInfo) {
        Folder folder = foInfo.getFolder(getController());
        if (folder != null) {
            return joinedByCloud(folder);
        }
        boolean folderInCloud = false;
        FolderList fList = server.getLastFolderList();
        ConnectionHandler conHan = server.getPeer();
        if (conHan != null && fList != null) {
            folderInCloud = fList.contains(foInfo, conHan.getMyMagicId());
        }
        // TODO: #2435
        return folderInCloud;
    }

    private void scheduleConnectHostingServers() {
        boolean currentlyHammering = currentlyHammeringServers();
        if (currentlyHammering) {
            logWarning("Detected hammering of server/cluster. Throttling reconnect speed. Next try in "
                    + HAMMER_DELAY / 1000 + "s");
        }
        getController().schedule(new HostingServersConnector(), currentlyHammering ? HAMMER_DELAY : 1000L);
    }

    /**
     * Tries to connect hosting servers of our locally joined folders. Call this
     * when it is expected, that any of the locally joined folders is hosted on
     * another server. This method does NOT block, it instead schedules a
     * background task to retrieve and connect those servers.
     */
    private void connectHostingServers() {
        if (!(isConnected() || isLoggingIn() || isLoggedIn())) {
            findAlternativeServer();
            return;
        }
        if (isFiner()) {
            logFiner("Connecting to cluster servers");
        }
        Runnable retriever = new Runnable() {
            @Override
            public void run() {
                retrieveAndConnectoClusterServers();
            }
        };
        getController().getIOProvider().startIO(retriever);
    }

    private void retrieveAndConnectoClusterServers() {
        try {
            if (ConfigurationEntry.SERVER_LOAD_NODES.getValueBoolean(getController())) {
                getController().getNodeManager().loadServerNodes(this);
            }

            if (!isConnected() || !isLoggedIn()) {
                return;
            }

            Collection<FolderInfo> infos = getController().getFolderRepository().getJoinedFolderInfos();
            FolderInfo[] folders = infos.toArray(new FolderInfo[infos.size()]);
            Collection<MemberInfo> hostingServers = getFolderService().getHostingServers(folders);
            if (isFine()) {
                logFine("Got " + hostingServers.size() + " servers for our " + folders.length + " folders: "
                        + hostingServers);
            }
            for (MemberInfo hostingServerInfo : hostingServers) {
                Member hostingServer = hostingServerInfo.getNode(getController(), true);
                hostingServer.updateInfo(hostingServerInfo);
                hostingServer.setServer(true);

                if (hostingServer.isConnected() || hostingServer.isConnecting() || hostingServer.equals(server)) {
                    // Already connected / reconnecting
                    continue;
                }
                // Connect now
                hostingServer.markForImmediateConnect();
            }
        } catch (Exception e) {
            logWarning("Unable to retrieve servers of cluster." + e);
        }
    }

    /**
     * Saves the infos of the server into the config properties. Does not save
     * the config file.
     *
     * @param newServer
     */
    public void setServerInConfig(MemberInfo newServer) {
        Reject.ifNull(newServer, "Server is null");

        ConfigurationEntry.SERVER_NAME.setValue(getController(), newServer.nick);
        // This probably causes a reverse lookup of the IP.
        String serverHost = newServer.getConnectAddress().getHostName();
        if (newServer.getConnectAddress().getPort() != ConnectionListener.DEFAULT_PORT) {
            serverHost += ':';
            serverHost += newServer.getConnectAddress().getPort();
        }
        ConfigurationEntry.SERVER_HOST.setValue(getController(), serverHost);
        if (isTempServerNode(newServer)) {
            ConfigurationEntry.SERVER_NODEID.removeValue(getController());
        } else {
            ConfigurationEntry.SERVER_NODEID.setValue(getController(), newServer.id);
        }
    }

    public boolean setServerWebURLInConfig(String newWebUrl) {
        String oldWebUrl = ConfigurationEntry.SERVER_WEB_URL.getValue(getController());
        if (Util.equals(oldWebUrl, newWebUrl)) {
            return false;
        }
        // Currently not supported from config
        if (StringUtils.isBlank(newWebUrl)) {
            ConfigurationEntry.SERVER_WEB_URL.removeValue(getController());
        } else {
            ConfigurationEntry.SERVER_WEB_URL.setValue(getController(), newWebUrl);
        }
        return true;
    }

    private boolean setServerHTTPTunnelURLInConfig(String newTunnelURL) {
        logFine("New tunnel URL: " + newTunnelURL);
        String oldUrl = ConfigurationEntry.SERVER_HTTP_TUNNEL_RPC_URL.getValue(getController());
        if (Util.equals(oldUrl, newTunnelURL)) {
            return false;
        }
        // #2158: Don't override if we are a sever itself.
        if (getController().getMySelf().isServer()) {
            return false;
        }
        // Currently not supported from config
        if (StringUtils.isBlank(newTunnelURL)) {
            ConfigurationEntry.SERVER_HTTP_TUNNEL_RPC_URL.removeValue(getController());
        } else {
            ConfigurationEntry.SERVER_HTTP_TUNNEL_RPC_URL.setValue(getController(), newTunnelURL);
        }
        return true;
    }

    // Event handling ********************************************************

    public void addListener(ServerClientListener listener) {
        ListenerSupportFactory.addListener(listenerSupport, listener);
    }

    public void addWeakListener(ServerClientListener listener) {
        ListenerSupportFactory.addListener(listenerSupport, listener, true);
    }

    public void removeListener(ServerClientListener listener) {
        ListenerSupportFactory.removeListener(listenerSupport, listener);
    }

    // Internal ***************************************************************

    private void setNewServerNode(Member newServerNode) {
        // Hammering detection
        if (server != null && newServerNode != null) {
            boolean changed = !server.equals(newServerNode);
            if (changed) {
                if (recentServerSwitch != null
                        && (System.currentTimeMillis() - recentServerSwitch.getTime()) > HAMMER_TIME) {
                    // Reset counter if last switch was "long" ago.
                    recentServerSwitches = 0;
                }
                recentServerSwitch = new Date();
                recentServerSwitches++;
            }
        }
        server = newServerNode;
        server.setServer(true);
        logInfo("New primary server: " + server.getNick());

        // Why?
        // // Put on friendslist
        // if (!isTempServerNode(server)) {
        // if (!server.isFriend()) {
        // server.setFriend(true, null);
        // }
        // }
        // Re-initalize the service stubs on new server node.
        initializeServiceStubs();
    }

    private static final long HAMMER_TIME = 10000L;
    private static final int HAMMER_HITS = 20;
    private static final long HAMMER_DELAY = 30000L;

    private boolean currentlyHammeringServers() {
        return recentServerSwitch != null
                && (System.currentTimeMillis() - recentServerSwitch.getTime()) <= HAMMER_TIME
                && recentServerSwitches >= HAMMER_HITS;
    }

    private void initializeServiceStubs() {
        securityService = getService(SecurityService.class);
        userService = getService(AccountService.class);
        folderService = getService(FolderService.class);
        publicKeyService = getService(PublicKeyService.class);
    }

    private void setAnonAccount() {
        accountDetails = new AccountDetails(new AnonymousAccount(), 0, 0);
    }

    private void saveLastKnowLogin(String username, String passwordObf) {
        if (StringUtils.isNotBlank(username)) {
            ConfigurationEntry.SERVER_CONNECT_USERNAME.setValue(getController(), username);
        } else {
            ConfigurationEntry.SERVER_CONNECT_USERNAME.removeValue(getController());
        }

        if (isRememberPassword() && StringUtils.isNotBlank(passwordObf)) {
            ConfigurationEntry.SERVER_CONNECT_PASSWORD.setValue(getController(), passwordObf);
        } else {
            ConfigurationEntry.SERVER_CONNECT_PASSWORD.removeValue(getController());
        }

        // Store new username/pw
        getController().saveConfig();
    }

    private void changeToServer(ServerInfo newServerInfo) {
        logFine("Changing server to " + newServerInfo.getNode());

        // Add key of new server to keystore.
        if (ProUtil.isRunningProVersion()
                && ProUtil.getPublicKey(getController(), newServerInfo.getNode()) == null) {
            try {
                PublicKey serverKey = publicKeyService.getPublicKey(newServerInfo.getNode());
                if (serverKey != null) {
                    logFine("Retrieved new key for server " + newServerInfo.getNode() + ". " + serverKey);
                    ProUtil.addNodeToKeyStore(getController(), newServerInfo.getNode(), serverKey);
                }
            } catch (RuntimeException e) {
                logWarning("Not changing server. Unable to retrieve new server key for " + newServerInfo.getName()
                        + ". " + e);
                return;
            }
        }

        // Get new server node from local p2p nodemanager database
        Member newServerNode = newServerInfo.getNode().getNode(getController(), true);

        // Remind new server for next connect.
        if (updateConfig) {
            if (newServerInfo.getNode().getConnectAddress() != null) {
                setServerInConfig(newServerInfo.getNode());
            } else {
                // Fallback, use node INFO from P2P database.
                setServerInConfig(newServerNode.getInfo());
            }
            setServerWebURLInConfig(newServerInfo.getWebUrl());
            setServerHTTPTunnelURLInConfig(newServerInfo.getHTTPTunnelUrl());
            getController().saveConfig();
        }

        // Now actually switch to new server.
        setNewServerNode(newServerNode);
        // Attempt to login. At least remind login for real connect.
        if (!isConnected()) {
            // Mark new server for connect
            server.markForImmediateConnect();
            Waiter w = new Waiter(1000);
            while (!w.isTimeout() && !isConnected()) {
                w.waitABit();
            }
            if (isConnected() && isFine()) {
                logFine("Connect success to " + server.getNick());
            }
        }
        login(username, passwordObf, true);
    }

    private void fireLogin(AccountDetails details) {
        fireLogin(details, true);
    }

    private void fireLogin(AccountDetails details, boolean loginSuccess) {
        listenerSupport.login(new ServerClientEvent(this, details, loginSuccess));
    }

    private void fireAccountUpdates(AccountDetails details) {
        listenerSupport.accountUpdated(new ServerClientEvent(this, details));
    }

    // General ****************************************************************

    public boolean showServerInfo() {
        if (getController().getDistribution().isBrandedClient()) {
            return false;
        }
        boolean pfCom = isPowerFolderCloud();
        boolean prompt = ConfigurationEntry.CONFIG_PROMPT_SERVER_IF_PF_COM.getValueBoolean(getController());
        return prompt || !pfCom;
    }

    /**
     * @return the string representing the server address
     */
    public String getServerString() {
        String addrStr;
        if (server != null) {
            if (server.isMySelf()) {
                addrStr = "myself";
            } else {
                InetSocketAddress addr = server.getReconnectAddress();
                if (addr != null) {
                    if (addr.getAddress() != null) {
                        addrStr = NetworkUtil.getHostAddressNoResolve(addr.getAddress());
                    } else {
                        addrStr = addr.getHostName();
                    }
                } else {
                    addrStr = "";
                }

                if (addr != null && addr.getPort() != ConnectionListener.DEFAULT_PORT) {
                    addrStr += ":" + addr.getPort();
                }
            }
        } else {
            addrStr = "";
        }
        if (hasWebURL()) {
            return getWebURL();
        } else if (StringUtils.isNotBlank(addrStr)) {
            return "pf://" + addrStr;
        } else {
            return "n/a";
        }

    }

    @Override
    public String toString() {
        return "ServerClient " + (username != null ? username : "?") + "@"
                + (server != null ? server.getNick() + "(" + server.getReconnectAddress() + ")" : "n/a");
    }

    // Inner classes **********************************************************

    public void primaryServerConnected(Member newNode) {
        ConnectionHandler conHan = newNode.getPeer();
        Identity id = conHan != null ? conHan.getIdentity() : null;
        supportsQuickLogin = id != null && id.isSupportsQuickLogin();
        if (supportsQuickLogin) {
            if (isFiner()) {
                logFiner("Quick login at server supported");
            }
            primaryServerConnected0(newNode);
        } else {
            logFine("Quick login at server NOT supported. Using regular login");
        }
    }

    private void primaryServerConnected0(Member newNode) {
        // Our server member instance is a temporary one. Lets get real.
        if (isTempServerNode(server)) {
            // Got connect to server! Take his ID and name.
            Member oldServer = server;
            setNewServerNode(newNode);
            // Remove old temporary server entry without ID.
            getController().getNodeManager().removeNode(oldServer);
            if (updateConfig) {
                setServerInConfig(server.getInfo());
                getController().saveConfig();
            }
            logInfo("Got connect to server: " + server + " nodeid: " + server.getId());
        }

        listenerSupport.serverConnected(new ServerClientEvent(this, newNode));

        if (username != null && StringUtils.isNotBlank(passwordObf)) {
            try {
                login(username, passwordObf, true);
                scheduleConnectHostingServers();
            } catch (Exception ex) {
                logWarning("Unable to login. " + ex);
                logFine(ex);
            }
        }

        // #2425
        if (ConfigurationEntry.SYNC_AND_EXIT.getValueBoolean(getController())) {
            // Check after 60 seconds. Then every 10 secs
            getController().performFullSync();
            getController().exitAfterSync(60);
        }
    }

    /**
     * This listener violates the rule "Listener/Event usage". Reason: Even when
     * a ServerClient is a true core-component there might be multiple
     * ClientServer objects that dynamically change.
     * <p>
     * http://dev.powerfolder.com/projects/powerfolder/wiki/GeneralDevelopRules
     */
    private class MyNodeManagerListener extends NodeManagerAdapter {
        @Override
        public void settingsChanged(NodeManagerEvent e) {
            // Transition Member.setServer(true)
            if (e.getNode().isServer()) {
                logInfo("Discovered new server of cluster(" + countServers() + "): " + e.getNode().getNick() + " @ "
                        + e.getNode().getReconnectAddress());
            } else if (getMySelf().isServer()) {
                logInfo("Not longer member of cluster: " + e.getNode().getNick() + " @ "
                        + e.getNode().getReconnectAddress());
            }
            listenerSupport.nodeServerStatusChanged(new ServerClientEvent(ServerClient.this, e.getNode()));
        }

        private int countServers() {
            int n = 0;
            for (Member node : getController().getNodeManager().getNodesAsCollection()) {
                if (node.isServer()) {
                    n++;
                }
            }
            return n;
        }

        @Override
        public void nodeConnected(NodeManagerEvent e) {
            if (e.getNode().isServer() && !isConnected()) {
                findAlternativeServer();
            }
            // #2366: Checked from via serverConnected(Member)
            if (ServerClient.this == getController().getOSClient() && supportsQuickLogin) {
                return;
            }
            // For JUnit tests only;
            if (isPrimaryServer(e.getNode())) {
                primaryServerConnected0(e.getNode());
            }
        }

        @Override
        public void nodeDisconnected(NodeManagerEvent e) {
            if (isPrimaryServer(e.getNode())) {
                findAlternativeServer();
                // Invalidate account.
                setAnonAccount();
                listenerSupport.serverDisconnected(new ServerClientEvent(ServerClient.this, e.getNode()));
            }
        }

        @Override
        public boolean fireInEventDispatchThread() {
            return false;
        }
    }

    private class MyFolderRepositoryListener implements FolderRepositoryListener {

        @Override
        public boolean fireInEventDispatchThread() {
            return false;
        }

        @Override
        public void folderRemoved(FolderRepositoryEvent e) {
        }

        @Override
        public void folderCreated(FolderRepositoryEvent e) {
            if (!getController().isStarted()) {
                return;
            }
            retrieveAndConnectoClusterServers();
        }

        @Override
        public void maintenanceStarted(FolderRepositoryEvent e) {
        }

        @Override
        public void maintenanceFinished(FolderRepositoryEvent e) {
        }
    }

    private class ServerConnectTask extends TimerTask {
        @Override
        public void run() {
            if (isConnected()) {
                return;
            }
            if (server.isMySelf()) {
                // Don't connect to myself
                return;
            }
            boolean allowLAN2Internet = ConfigurationEntry.SERVER_CONNECT_FROM_LAN_TO_INTERNET
                    .getValueBoolean(getController());
            if (!allowLAN2Internet && getController().isLanOnly() && !server.isOnLAN()) {
                logFiner("NOT connecting to server: " + server + ". Reason: Server not on LAN");
                return;
            }
            if (!getController().getNodeManager().isStarted()
                    || !getController().getReconnectManager().isStarted()) {
                return;
            }
            if (server.isConnecting() || server.isConnected()) {
                return;
            }
            // Try to connect
            server.markForImmediateConnect();

            if (server.isUnableToConnect()) {
                // Try to connect to any known server
                findAlternativeServer();
            }
        }
    }

    private class AutoLoginTask extends TimerTask {
        @Override
        public void run() {
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    if (!isConnected()) {
                        return;
                    }
                    if (isLoggingIn()) {
                        return;
                    }
                    try {
                        // PFC-2368: Verify login by server too.
                        if (isLoggedIn() && securityService.isLoggedIn()) {
                            return;
                        }
                    } catch (RemoteCallException e) {
                        logFine("Problems with the connection to: " + getServerString() + ". " + e);
                        return;
                    }
                    try {
                        if (username != null && (StringUtils.isNotBlank(passwordObf)
                                || (StringUtils.isBlank(passwordObf) && ConfigurationEntry.KERBEROS_SSO_ENABLED
                                        .getValueBoolean(getController())))) {
                            logInfo("Auto-Login: Logging in " + username);
                            login(username, passwordObf, true);
                        }
                    } catch (RemoteCallException e) {
                        logWarning("Unable to automatically login at: " + username + " @ " + getServerString()
                                + ". " + e);
                    }
                }
            };
            getController().getIOProvider().startIO(r);
        }
    }

    /**
     * Task to retrieve hosting Online Storage servers which host my files.
     */
    private class HostingServersConnector extends TimerTask {
        @Override
        public void run() {
            connectHostingServers();
        }
    }

    private class MyThrowableHandler implements ThrowableHandler {
        private int loginProblems;

        @Override
        public void handle(Throwable t) {
            if (t instanceof NotLoggedInException) {
                autoLogin(t);
            } else if (t instanceof AuthenticationFailedException) {
                // NOP - PFC-2589
            } else if (t instanceof SecurityException) {
                if (t.getMessage() != null && t.getMessage().toLowerCase().contains("not logged")) {
                    autoLogin(t);
                }
            }
        }

        private void autoLogin(Throwable t) {
            if (username != null && StringUtils.isNotBlank(passwordObf)) {
                loginProblems++;
                if (loginProblems > 20) {
                    logSevere("Got " + loginProblems + " login problems. "
                            + "Not longer auto-logging in to prevent hammering server.");
                    return;
                }
                logWarning("Auto-login for " + username + " required. Caused by " + t);
                try {
                    login(username, passwordObf, true);
                } catch (Exception e) {
                    logWarning("Unable to login with " + username + " at " + getServerString() + ". " + e);
                }
            }
        }
    }

    private class ServiceTicketGenerator implements PrivilegedExceptionAction<byte[]> {
        @Override
        public byte[] run() throws Exception {
            try {
                Oid kerberos5Oid = new Oid("1.2.840.113554.1.2.2");
                GSSManager gssManager = GSSManager.getInstance();
                GSSName clientName = gssManager.createName(username, GSSName.NT_USER_NAME);
                GSSName serviceName = gssManager
                        .createName(ConfigurationEntry.KERBEROS_SSO_SERVICE_NAME.getValue(getController()) + "@"
                                + ConfigurationEntry.KERBEROS_SSO_REALM.getValue(getController()), null);

                GSSCredential clientCredentials = gssManager.createCredential(clientName, 8 * 60 * 60, kerberos5Oid,
                        GSSCredential.INITIATE_ONLY);

                GSSContext gssContext = gssManager.createContext(serviceName, kerberos5Oid, clientCredentials,
                        GSSContext.DEFAULT_LIFETIME);

                byte[] serviceTicket = gssContext.initSecContext(new byte[0], 0, 0);
                gssContext.dispose();
                return serviceTicket;
            } catch (Exception e) {
                logWarning(e);
                return null;
            }

        }
    }
}