/*
* 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.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
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.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
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.distribution.AbstractDistribution;
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.NotLoggedInException;
import de.dal33t.powerfolder.security.SecurityException;
import de.dal33t.powerfolder.util.Base64;
import de.dal33t.powerfolder.util.IdGenerator;
import de.dal33t.powerfolder.util.LoginUtil;
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;
/**
* 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 Member server;
private MyThrowableHandler throwableHandler = new MyThrowableHandler();
private final AtomicBoolean loggingIn = new AtomicBoolean();
/**
* 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();
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);
}
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");
listenerSupport = ListenerSupportFactory
.createListenerSupport(ServerClientListener.class);
setNewServerNode(serverNode);
// Allowed by default
allowServerChange = serverChange;
setAnonAccount();
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 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);
}
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;
}
if (!ConfigurationEntry.WEB_PASSWORD_ALLOWED
.getValueBoolean(getController()))
{
return getWebURL();
}
return LoginUtil.decorateURL(getWebURL() + Constants.LOGIN_URI,
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);
}
/**
* @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 = getController().getOSClient()
.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);
}
/**
* @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;
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() + "/register";
}
/**
* 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() + "/activate";
}
/**
* @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");
if (StringUtils.isBlank(un)
&& LoginUtil.isValidUsername(getController(), systemUserName))
{
un = systemUserName;
}
if (StringUtils.isBlank(un)) {
logFine("Not logging in. Username blank");
} else {
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();
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));
}
/**
* 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
* @return the identity with this username or <code>InvalidAccount</code> if
* login failed. NEVER returns <code>null</code>
*/
private Account login(String theUsername, String thePasswordObj) {
logFine("Login with: " + theUsername);
synchronized (loginLock) {
loggingIn.set(true);
try {
username = theUsername;
passwordObf = thePasswordObj;
saveLastKnowLogin();
if (!server.isConnected() || StringUtils.isBlank(passwordObf)) {
// if (!server.isConnected()) {
// findAlternativeServer();
// }
setAnonAccount();
fireLogin(accountDetails);
return accountDetails.getAccount();
}
boolean loginOk = false;
char[] pw = LoginUtil.deobfuscate(passwordObf);
try {
loginOk = securityService.login(username, pw);
} 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
fireLogin(accountDetails);
getController().schedule(new Runnable() {
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);
setAnonAccount();
fireLogin(accountDetails, false);
return accountDetails.getAccount();
} finally {
loggingIn.set(false);
}
}
}
private void findAlternativeServer() {
if (!allowServerChange) {
return;
}
if (getController().getMySelf().isServer()) {
// Don't
return;
}
if (getController().isShuttingDown() || !getController().isStarted()) {
return;
}
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);
}
}
}
}
}
/**
* Are we currently logging in?
*
* @return
*/
public boolean isLoggingIn() {
return loggingIn.get();
}
/**
* @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 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);
}
/**
* 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 delayedAndChecked = currentlyHammeringServers()
|| !targetServerNode.isConnected();
if (!delayedAndChecked) {
logInfo("Switching from " + server.getNick() + " to "
+ targetServerNode.getNick());
changeToServer(targetServer);
} else {
logInfo("Switching from " + server.getNick() + " to "
+ targetServerNode.getNick() + " in " + HAMMER_DELAY / 1000
+ "s");
try {
Thread.sleep(HAMMER_DELAY);
} catch (InterruptedException e) {
logFiner(e);
return;
}
getController().getIOProvider().startIO(new Runnable() {
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() {
public void run() {
retrieveAndConnectoClusterServers();
}
};
getController().getIOProvider().startIO(retriever);
}
private void retrieveAndConnectoClusterServers() {
try {
if (ConfigurationEntry.SERVER_LOAD_NODES
.getValueBoolean(getController()))
{
getController().getNodeManager().loadServerNodes();
}
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);
// 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() {
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);
}
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 = AbstractDistribution
.isPowerFolderServer(getController());
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";
}
}
public String toString() {
return "ServerClient to " + (server != null ? server : "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);
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 a new server of " + countServers()
+ " in cluster: " + e.getNode().getNick() + " @ "
+ e.getNode().getReconnectAddress());
} else {
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;
}
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());
}
}
public void nodeDisconnected(NodeManagerEvent e) {
if (isPrimaryServer(e.getNode())) {
findAlternativeServer();
// Invalidate account.
setAnonAccount();
listenerSupport.serverDisconnected(new ServerClientEvent(
ServerClient.this, e.getNode()));
}
}
public boolean fireInEventDispatchThread() {
return false;
}
}
private class MyFolderRepositoryListener implements
FolderRepositoryListener
{
public boolean fireInEventDispatchThread() {
return false;
}
public void folderRemoved(FolderRepositoryEvent e) {
}
public void folderCreated(FolderRepositoryEvent e) {
if (!getController().isStarted()) {
return;
}
retrieveAndConnectoClusterServers();
}
public void maintenanceStarted(FolderRepositoryEvent e) {
}
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() {
if (!isConnected()) {
return;
}
if (isLoggedIn()) {
return;
}
if (isLoggingIn()) {
return;
}
if (username != null && StringUtils.isNotBlank(passwordObf)) {
login(username, passwordObf);
}
}
}
/**
* 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;
public void handle(Throwable t) {
if (t instanceof NotLoggedInException) {
autoLogin(t);
} 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);
} catch (Exception e) {
logWarning("Unable to login with " + username + " at "
+ getServerString() + ". " + e);
}
}
}
}
}