blob: 94962db6c0a95e3ee5cf7bc99421a6c0129b29c0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2002, 2015 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Initial Contributors:
* The following IBM employees contributed to the Remote System Explorer
* component that contains this file: David McKnight, Kushal Munir,
* Michael Berger, David Dykstal, Phil Coulthard, Don Yantzi, Eric Simpson,
* Emily Bruner, Mazen Faraj, Adrian Storisteanu, Li Ding, and Kent Hawley.
*
* Contributors:
* David McKnight (IBM) [220123][dstore] Configurable timeout on irresponsiveness
* David McKnight (IBM) [220892][dstore] Backward compatibility: Server and Daemon should support old clients
* Noriaki Takatsu (IBM) - [220126] [dstore][api][breaking] Single process server for multiple clients
* David McKnight (IBM) [224906] [dstore] changes for getting properties and doing exit due to single-process capability
* Jacob Garcowski (IBM) [225175] [dstore] error handling change for Client
* David McKnight (IBM) - [225507][api][breaking] RSE dstore API leaks non-API types
* Noriaki Takatsu (IBM) - [226074] process for getStatus() API
* Noriaki Takatsu (IBM) - [226237] [dstore] Move the place where the ServerLogger instance is made
* David McKnight (IBM) - [226561] [apidoc] Add API markup to RSE Javadocs where extend / implement is allowed
* Noriaki Takatsu (IBM) - [242968] [multithread] serverSocket must be closed when an exception happens in Accept
* David McKnight (IBM) - [257321] [dstore] "Error binding socket" should include port of the failed socket
* Noriaki Takatsu (IBM) - [283656] [dstore][multithread] Serviceability issue
* Noriaki Takatsu (IBM) - [289678][api][breaking] ServerSocket creation in multiple IP addresses
* David McKnight (IBM) - [368072] [dstore][ssl] no exception logged upon bind error
* David McKnight (IBM) - [371401] [dstore][multithread] avoid use of static variables - causes memory leak after disconnect
* David McKnight (IBM) - [388472] [dstore] need alternative option for getting at server hostname
* David McKnight (IBM) - [390681] [dstore] need to merge differences between HEAD stream and 3.2 in ConnectionEstablisher.finished()
* David McKnight (IBM) [439545][dstore] potential deadlock on senders during shutdown
* David McKnight (IBM) [464736][dstore] need methods to disable ciphers and protocols
* David McKnight (IBM) [473086][dstore] enableProtocols() calls setEnabledCipherSuites(), not setEnabledProtocols()
*******************************************************************************/
package org.eclipse.dstore.core.server;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import org.eclipse.dstore.core.model.Client;
import org.eclipse.dstore.core.model.DE;
import org.eclipse.dstore.core.model.DataElement;
import org.eclipse.dstore.core.model.DataStore;
import org.eclipse.dstore.core.model.DataStoreAttributes;
import org.eclipse.dstore.core.model.ISSLProperties;
import org.eclipse.dstore.internal.core.server.ServerAttributes;
import org.eclipse.dstore.internal.core.server.ServerCommandHandler;
import org.eclipse.dstore.internal.core.server.ServerReturnCodes;
import org.eclipse.dstore.internal.core.server.ServerSSLProperties;
import org.eclipse.dstore.internal.core.server.ServerUpdateHandler;
import org.eclipse.dstore.internal.core.util.ExternalLoader;
import org.eclipse.dstore.internal.core.util.Sender;
import org.eclipse.dstore.internal.core.util.ssl.DStoreSSLContext;
/**
* ConnectionEstablisher is responsible for managing the server DataStore and
* facilitating the communication between client and server DataStores.
*
* @noextend This class is not intended to be subclassed by clients.
* @noinstantiate This class is not intended to be instantiated by clients.
*
* @since 3.0 moved from non-API to API
*/
public class ConnectionEstablisher
{
private ServerSocket _serverSocket;
private boolean _continue;
private ArrayList _receivers;
private ServerCommandHandler _commandHandler;
private ServerUpdateHandler _updateHandler;
private ServerAttributes _serverAttributes = new ServerAttributes();
private DataStore _dataStore;
private int _maxConnections;
private int _timeout;
private String _msg;
private String[] _disabledCipherPatterns = null;
private String[] _disabledProtocolPatterns = null;
private String[] _enabledCiphers = null;
private String[] _enabledProtocols = null;
/**
* Creates the default ConnectionEstablisher. Communication occurs
* on a default port, there is no timeout and no ticket is required
* for a client to work with the DataStore.
*
*/
public ConnectionEstablisher()
{
String port = _serverAttributes.getAttribute(DataStoreAttributes.A_HOST_PORT);
setup(port, null, null);
}
/**
* Creates a ConnectionEstablisher. Communication occurs
* on the specified port, there is no timeout and no ticket is required
* for a client to work with the DataStore.
*
* @param port the number of the socket port
*/
public ConnectionEstablisher(String port)
{
setup(port, null, null);
}
/**
* Creates a ConnectionEstablisher. Communication occurs
* on the specified port, a timeout value indicates the idle wait
* time before shutting down, and no ticket is required
* for a client to work with the DataStore.
*
* @param port the number of the socket port
* @param timeout the idle duration to wait before shutting down
*/
public ConnectionEstablisher(String port, String timeout)
{
setup(port, timeout, null);
}
/**
* Creates a ConnectionEstablisher. Communication occurs
* on the specified port, a timeout value indicates the idle wait
* time before shutting down, and ticket specified the required
* ticket for a client to present in order to work with the DataStore.
*
* @param port the number of the socket port
* @param timeout the idle duration to wait before shutting down
* @param ticket validation id required by the client to access the DataStore
*/
public ConnectionEstablisher(String port, String timeout, String ticket)
{
setup(port, timeout, ticket);
}
/**
* Creates a ConnectionEstablisher. Communication occurs
* on the specified port and the specified IP address,
* a timeout value indicates the idle wait time
* before shutting down, and ticket specified the required
* ticket for a client to present in order to work with the DataStore.
*
* @param port the number of the socket port
* @param backlog listen backlog
* @param bindAddr the local IP address to bind to
* @param timeout the idle duration to wait before shutting down
* @param ticket validation id required by the client to access the DataStore
* @since 3.2
*/
public ConnectionEstablisher(String port, int backlog, InetAddress bindAddr, String timeout, String ticket)
{
setup(port, backlog, bindAddr, timeout, ticket);
}
/**
* Starts the run loop for the ConnectionEstablisher.
*/
public void start()
{
run();
}
/**
* Returns the DataStore.
*
* @return the DataStore
*/
public DataStore getDataStore()
{
return _dataStore;
}
/**
* Return the Server port opened for this client
*
* @return the Server port opened for this client
*/
public int getServerPort()
{
if (_serverSocket != null)
{
return _serverSocket.getLocalPort();
}
return -1;
}
/**
* Return the connection status for this client
*
* * @return the connection status for this client
*/
public String getStatus()
{
return _msg;
}
/**
* Tells the connection establisher to clean up and shutdown
*/
public void finished(ServerReceiver receiver)
{
if (_dataStore.getClient() != null) {
_dataStore.getClient().getLogger().logInfo(this.getClass().toString(), "ConnectionEstablisher.finished()"); //$NON-NLS-1$
}
if (_dataStore.getClient() != null) {
_dataStore.getClient().getLogger().logInfo(this.getClass().toString(), "ConnectionEstablisher - removing receiver"); //$NON-NLS-1$
}
_receivers.remove(receiver);
if (_dataStore.getClient() != null) {
_dataStore.getClient().getLogger().logInfo(this.getClass().toString(), "ConnectionEstablisher - removing preference listener"); //$NON-NLS-1$
}
_dataStore.removeDataStorePreferenceListener(receiver);
//if (_receivers.size() == 0)
{
_continue = false;
_commandHandler.finish();
if (_dataStore.getClient() != null) {
_dataStore.getClient().getLogger().logInfo(this.getClass().toString(), "ConnectionEstablisher - finishing update handler"); //$NON-NLS-1$
}
_updateHandler.finish();
if (_dataStore.getClient() != null) {
_dataStore.getClient().getLogger().logInfo(this.getClass().toString(), "ConnectionEstablisher - removing sender"); //$NON-NLS-1$
}
_updateHandler.removeSenderWith(receiver.socket());
if (_dataStore.getClient() != null) {
_dataStore.getClient().getLogger().logInfo(this.getClass().toString(), "ConnectionEstablisher - finishing DataStore"); //$NON-NLS-1$
}
_dataStore.finish();
System.out.println(ServerReturnCodes.RC_FINISHED);
if (SystemServiceManager.getInstance().getSystemService() == null)
System.exit(0);
}
}
private void waitForConnections()
{
while (_continue == true)
{
try
{
if (_dataStore.usingSSL())
{
SSLServerSocket sslServerSocket = (SSLServerSocket)_serverSocket;
// for security, disable ciphers and protocols we don't want
disableCiphers(sslServerSocket);
disableProtocols(sslServerSocket);
// for security, enable only ciphers and protocols that are common
enableCiphers(sslServerSocket);
enableProtocols(sslServerSocket);
logAvailableCiphersAndProtocols(sslServerSocket);
}
Socket newSocket = _serverSocket.accept();
if (_dataStore.usingSSL())
{
// wait for connection
SSLSocket sslSocket = (SSLSocket)newSocket;
sslSocket.setUseClientMode(false);
sslSocket.setNeedClientAuth(false);
SSLSession session = sslSocket.getSession();
if (session == null)
{
System.out.println("handshake failed"); //$NON-NLS-1$
sslSocket.close();
return;
}
}
doHandShake(newSocket);
newSocket.setKeepAlive(true);
ServerReceiver receiver = new ServerReceiver(newSocket, this);
_dataStore.addDataStorePreferenceListener(receiver);
if (_dataStore.getClient() != null)
_dataStore.getClient().setServerReceiver(receiver);
Sender sender = new Sender(newSocket, _dataStore);
// add this connection to list of elements
_receivers.add(receiver);
_updateHandler.addSender(sender);
receiver.start();
if (_receivers.size() == 1)
{
_updateHandler.start();
_commandHandler.start();
}
if (_receivers.size() == _maxConnections)
{
_continue = false;
_serverSocket.close();
}
}
catch (IOException ioe)
{
System.err.println(ServerReturnCodes.RC_CONNECTION_ERROR);
System.err.println("Server: error initializing socket: " + ioe); //$NON-NLS-1$
_msg = ioe.toString();
try
{
_serverSocket.close();
}
catch (Throwable e)
{
}
_continue = false;
}
}
}
private ServerSocket createSocket(String portStr, int backlog, InetAddress bindAddr) throws UnknownHostException
{
ServerSocket serverSocket = null;
SSLContext sslContext = null;
// port
int port = 0;
if (_dataStore.usingSSL())
{
String keyStoreFileName = _dataStore.getKeyStoreLocation();
String keyStorePassword = _dataStore.getKeyStorePassword();
try
{
sslContext = DStoreSSLContext.getServerSSLContext(keyStoreFileName, keyStorePassword);
}
catch (Exception e)
{
}
}
// determine if portStr is a port range or just a port
String[] range = portStr.split("-"); //$NON-NLS-1$
if (range.length == 2)
{
int lPort = 0;
int hPort = 0;
try
{
lPort = Integer.parseInt(range[0]);
hPort = Integer.parseInt(range[1]);
}
catch (Exception e)
{
}
for (int i = lPort; i < hPort; i++)
{
// create server socket from port
try
{
if (_dataStore.usingSSL() && sslContext != null)
{
try
{
serverSocket = sslContext.getServerSocketFactory().createServerSocket(i, backlog, bindAddr);
}
catch (BindException e)
{
_msg = ServerReturnCodes.RC_BIND_ERROR + " on port " + port + ": " + e.getMessage(); //$NON-NLS-1$ //$NON-NLS-2$
System.err.println(_msg);
_dataStore.trace(_msg);
}
catch (Exception e)
{
}
}
else
{
try
{
serverSocket = new ServerSocket(i, backlog, bindAddr);
}
catch (BindException e)
{
_msg = ServerReturnCodes.RC_BIND_ERROR + " on port " + port + ": " + e.getMessage(); //$NON-NLS-1$ //$NON-NLS-2$
System.err.println(_msg);
_dataStore.trace(_msg);
}
catch (Exception e)
{
}
}
}
catch (Exception e)
{
_dataStore.trace(e);
}
if (serverSocket != null && serverSocket.getLocalPort() > 0)
{
return serverSocket;
}
}
if (serverSocket == null){
_msg = ServerReturnCodes.RC_BIND_ERROR + " on ports " + portStr; //$NON-NLS-1$
System.err.println(_msg);
}
}
else
{
port = Integer.parseInt(portStr);
// create server socket from port
if (_dataStore.usingSSL() && sslContext != null)
{
try
{
serverSocket = sslContext.getServerSocketFactory().createServerSocket(port, backlog, bindAddr);
}
catch (BindException e){
_msg = ServerReturnCodes.RC_BIND_ERROR + " on port " + port + ": " + e.getMessage(); //$NON-NLS-1$ //$NON-NLS-2$
System.err.println(_msg);
_dataStore.trace(_msg);
}
catch (Exception e)
{
_dataStore.trace(e);
}
}
else
{
try
{
serverSocket = new ServerSocket(port, backlog, bindAddr);
}
catch (BindException e){
_msg = ServerReturnCodes.RC_BIND_ERROR + " on port " + port + ": " + e.getMessage(); //$NON-NLS-1$ //$NON-NLS-2$
System.err.println(_msg);
_dataStore.trace(_msg);
}
catch (Exception e)
{
_dataStore.trace(e);
}
}
}
return serverSocket;
}
/**
* Create the DataStore and initializes it's handlers and communications.
*
* @param portStr the number of the socket port
* @param timeoutStr the idle duration to wait before shutting down
* @param ticketStr validation id required by the client to access the DataStore
*/
private void setup(String portStr, String timeoutStr, String ticketStr)
{
setup(portStr, 50, null, timeoutStr, ticketStr);
}
/**
* Create the DataStore and initializes it's handlers and communications.
*
* @param portStr the number of the socket port
* @param backlog listen backlog
* @param bindAddr the local IP address to bind to
* @param timeoutStr the idle duration to wait before shutting down
* @param ticketStr validation id required by the client to access the DataStore
*/
private void setup(String portStr, int backlog, InetAddress bindAddr, String timeoutStr, String ticketStr)
{
_maxConnections = 1;
ArrayList loaders = new ArrayList();
loaders.add(new ExternalLoader(getClass().getClassLoader(), "*")); //$NON-NLS-1$
_commandHandler = new ServerCommandHandler(loaders);
_updateHandler = new ServerUpdateHandler();
ISSLProperties sslProperties = new ServerSSLProperties();
_dataStore = new DataStore(_serverAttributes, _commandHandler, _updateHandler, null);
_dataStore.setSSLProperties(sslProperties);
DataElement ticket = _dataStore.getTicket();
ticket.setAttribute(DE.A_NAME, ticketStr);
_updateHandler.setDataStore(_dataStore);
_commandHandler.setDataStore(_dataStore);
if (SystemServiceManager.getInstance().getSystemService() == null)
{
Client client = new Client();
_dataStore.setClient(client);
ServerLogger logger = new ServerLogger(_dataStore.getUserPreferencesDirectory());
client.setLogger(logger);
}
_receivers = new ArrayList();
_continue = true;
try
{
_serverSocket = createSocket(portStr, backlog, bindAddr);
if (_serverSocket == null)
{
_continue = false;
}
else
{
// timeout
if (timeoutStr != null)
{
_timeout = Integer.parseInt(timeoutStr);
}
else
{
_timeout = 120000;
}
if (_timeout > 0)
{
_serverSocket.setSoTimeout(_timeout);
}
System.err.println(ServerReturnCodes.RC_SUCCESS);
System.err.println(_serverSocket.getLocalPort());
_msg = ServerReturnCodes.RC_SUCCESS;
System.err.println("Server running on: " + ServerAttributes.getHostName()); //$NON-NLS-1$
}
}
catch (UnknownHostException e)
{
System.err.println(ServerReturnCodes.RC_UNKNOWN_HOST_ERROR + ':' + e.getMessage());
_msg = ServerReturnCodes.RC_UNKNOWN_HOST_ERROR;
_continue = false;
}
catch (BindException e)
{
System.err.println(ServerReturnCodes.RC_BIND_ERROR + ':' + e.getMessage());
_msg = ServerReturnCodes.RC_BIND_ERROR;
_continue = false;
}
catch (IOException e)
{
System.err.println(ServerReturnCodes.RC_GENERAL_IO_ERROR + ':' + e.getMessage());
_msg = ServerReturnCodes.RC_GENERAL_IO_ERROR;
_continue = false;
}
catch (SecurityException e)
{
System.err.println(ServerReturnCodes.RC_SECURITY_ERROR + ':' + e.getMessage());
_msg = ServerReturnCodes.RC_SECURITY_ERROR;
_continue = false;
}
}
private void run()
{
waitForConnections();
}
private void doHandShake(Socket socket)
{
try
{
BufferedWriter bwriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), DE.ENCODING_UTF_8));
PrintWriter writer = new PrintWriter(bwriter);
String version = DataStoreAttributes.DATASTORE_VERSION;
String preferenceVersion = System.getProperty("DSTORE_VERSION"); //$NON-NLS-1$
if (preferenceVersion != null && preferenceVersion.length() > 0){
version = preferenceVersion;
}
writer.println(version);
writer.flush();
if (socket instanceof SSLSocket){ // log the protocol and cipher suite used
SSLSocket sslSocket = (SSLSocket)socket;
SSLSession session = sslSocket.getSession();
String protocol = session.getProtocol();
String cipherSuite = session.getCipherSuite();
IServerLogger logger = _dataStore.getClient().getLogger();
String cn = getClass().toString();
logger.logInfo(cn, "SSL/TLS Protocol: "+protocol); //$NON-NLS-1$
logger.logInfo(cn, "SSL/TLS Cipher Suite: " + cipherSuite); //$NON-NLS-1$
}
}
catch (IOException e)
{
if (_dataStore.getClient() != null) {
_dataStore.getClient().getLogger().logError(this.getClass().toString(), e.toString(), e);
}
System.out.println(e);
}
}
private void logAvailableCiphersAndProtocols(SSLServerSocket sslSocket){
IServerLogger logger = _dataStore.getClient().getLogger();
String cn = getClass().toString();
// list the supported and available ciphers and protocols
logger.logDebugMessage(cn, "SSL/TLS Enabled Cipher Suites:"); //$NON-NLS-1$
String[] enabledSuites = sslSocket.getEnabledCipherSuites();
for (int i = 0; i < enabledSuites.length; i++){
String suite = enabledSuites[i];
logger.logDebugMessage(cn, '\t' + suite);
}
String[] supportedSuites = sslSocket.getSupportedCipherSuites();
logger.logDebugMessage(cn, "SSL/TLS Supported Cipher Suites:"); //$NON-NLS-1$
for (int i = 0; i < supportedSuites.length; i++){
String suite = supportedSuites[i];
logger.logDebugMessage(cn, '\t' + suite);
}
String[] enabledProtocols = sslSocket.getEnabledProtocols();
logger.logDebugMessage(cn, "SSL/TLS Enabled Protocols:"); //$NON-NLS-1$
for (int i = 0; i < enabledProtocols.length; i++){
String eprotocol = enabledProtocols[i];
logger.logDebugMessage(cn, '\t' + eprotocol);
}
String[] supportedProtocols = sslSocket.getSupportedProtocols();
logger.logDebugMessage(cn, "SSL/TLS Supported Protocols:"); //$NON-NLS-1$
for (int i = 0; i < supportedProtocols.length; i++){
String sprotocol = supportedProtocols[i];
logger.logDebugMessage(cn, '\t' + sprotocol);
}
}
/**
* Specify cipher patterns to be disabled when using SSL sockets
* @param cipherPatterns regex patterns of ciphers to disable
*/
public void setDisabledCipherPatterns(String[] cipherPatterns){
_disabledCipherPatterns = cipherPatterns;
}
/**
* Specify protocol patterns to be disabled when using SSL sockets
* @param protocolPatterns regex patterns of protocols to disable
*/
public void setDisabledProtocolPatterns(String[] protocolPatterns){
_disabledProtocolPatterns = protocolPatterns;
}
/**
* Specify ciphers to be enabled when using SSL sockets
* @param ciphers to enable
*/
public void setEnabledCiphers(String[] ciphers){
_enabledCiphers = ciphers;
}
/**
* Specify protocols to be enabled when using SSL sockets
* @param protocols to enable
*/
public void setEnabledProtocols(String[] protocols){
_enabledProtocols = protocols;
}
private String[] filterNames(String[] inNames, String[] filters){
List outNames = new ArrayList();
for (int n = 0; n < inNames.length; n++){
String inName = inNames[n];
boolean match = false;
for (int i = 0; i < filters.length && !match; i++){
String filter = filters[i];
match = inName.matches(filter);
}
if (!match){
outNames.add(inName);
}
else {
String cn = getClass().toString();
IServerLogger logger = _dataStore.getClient().getLogger();
logger.logDebugMessage(cn, "Filtering out: " + inName); //$NON-NLS-1$
}
}
return (String[])outNames.toArray(new String[outNames.size()]);
}
private void disableCiphers(SSLServerSocket socket){
if (_disabledCipherPatterns != null){
String[] enabledSuites = socket.getEnabledCipherSuites();
String[] newEnabledSuites = filterNames(enabledSuites, _disabledCipherPatterns);
socket.setEnabledCipherSuites(newEnabledSuites);
}
}
private void disableProtocols(SSLServerSocket socket){
if (_disabledProtocolPatterns != null){
String[] enabledProtocols = socket.getEnabledProtocols();
String[] newEnabledProtocols = filterNames(enabledProtocols, _disabledProtocolPatterns);
socket.setEnabledProtocols(newEnabledProtocols);
}
}
private String[] mergeCommon(String[] inNames1, String[] inNames2){
List merged = new ArrayList();
for (int n = 0; n < inNames1.length; n++){
String inName1 = inNames1[n];
boolean match = false;
for (int i = 0; i < inNames2.length && !match; i++){
match = inName1.equals(inNames2[i]);
}
if (match){
merged.add(inName1);
}
}
return (String[])merged.toArray(new String[merged.size()]);
}
private void enableCiphers(SSLServerSocket socket){
if (_enabledCiphers != null){
String[] enabledSuites = socket.getEnabledCipherSuites();
String[] newEnabledSuites = mergeCommon(enabledSuites, _enabledCiphers);
if (newEnabledSuites.length > 0){
socket.setEnabledCipherSuites(newEnabledSuites);
}
}
}
private void enableProtocols(SSLServerSocket socket){
if (_enabledProtocols != null){
String[] enabledProtocols = socket.getEnabledProtocols();
String[] newEnabledProtocols = mergeCommon(enabledProtocols, _enabledProtocols);
if (newEnabledProtocols.length > 0){
socket.setEnabledProtocols(newEnabledProtocols);
}
}
}
}