/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.clientapi; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.maven.artifact.versioning.ComparableVersion; import org.jboss.remoting.Client; import org.jboss.remoting.InvokerLocator; import org.jboss.remoting.invocation.NameBasedInvocation; import org.jboss.remoting.security.SSLSocketBuilder; import org.jboss.remoting.transport.http.ssl.HTTPSClientInvoker; import org.rhq.bindings.client.AbstractRhqFacade; import org.rhq.bindings.client.RhqManager; import org.rhq.bindings.util.InterfaceSimplifier; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.common.ProductInfo; import org.rhq.enterprise.communications.util.SecurityUtil; import org.rhq.enterprise.server.auth.SubjectManagerRemote; import org.rhq.enterprise.server.system.SystemManagerRemote; /** * A remote access client that provides transparent servlet-based proxies to an RHQ Server. * * @author Greg Hinkle * @author Simeon Pinder * @author Jay Shaughnessy * @author John Mazzitelli */ public class RemoteClient extends AbstractRhqFacade { private static final Log LOG = LogFactory.getLog(RemoteClient.class); public static final String NONSECURE_TRANSPORT = "servlet"; public static final String SECURE_TRANSPORT = "sslservlet"; private String transport; private final String host; private final int port; private boolean loggedIn; private boolean connected; private Map<RhqManager, Object> managers; private Subject subject; private Client remotingClient; private String subsystem = null; private ProductInfo serverInfo = null; /** * Creates a client that will communicate with the server running on the given host * listening on the given port. This constructor will not attempt to connect or login * to the remote server - use {@link #login(String, String)} for that. * * @param host * @param port */ public RemoteClient(String host, int port) { this(null, host, port); } /** * Creates a client that will communicate with the server running on the given host * listening on the given port over the given transport. * This constructor will not attempt to connect or login * to the remote server - use {@link #login(String, String)} for that. * * @param transport valid values are "servlet" and "sslservlet" - if <code>null</code>, * sslservlet will be used for ports that end with "443", servlet otherwise * @param host * @param port */ public RemoteClient(String transport, String host, int port) { this(transport, host, port, null); } public RemoteClient(String transport, String host, int port, String subsystem) { this.transport = (transport != null) ? transport : guessTransport(port); this.host = host; this.port = port; this.subsystem = subsystem; } public <T> T remoteInvoke(RhqManager manager, Method method, Class<T> expectedReturnType, Object... parameters) throws Throwable { String methodSig = manager.remote().getName() + ":" + method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); String[] paramSig = new String[paramTypes.length]; for (int x = 0; x < paramTypes.length; x++) { paramSig[x] = paramTypes[x].getName(); } NameBasedInvocation request = new NameBasedInvocation(methodSig, parameters, paramSig); Object response = getRemotingClient().invoke(request); if (response instanceof Throwable) { throw (Throwable) response; } return response == null ? null : expectedReturnType.cast(response); } /** * Connects to the remote server and logs in with the given credentials. * After successfully executing this, {@link #isLoggedIn()} will be <code>true</code> * and {@link #getSubject()} will return the subject that this method returns. * * @param user * @param password * * @return the logged in user * * @throws Exception if failed to connect to the server or log in */ public Subject login(String user, String password) throws Exception { logout(); doConnect(); Method loginMethod = SubjectManagerRemote.class.getDeclaredMethod("login", String.class, String.class); try { this.subject = remoteInvoke(RhqManager.SubjectManager, loginMethod, Subject.class, user, password); } catch (Exception e) { throw e; } catch (Throwable e) { throw new Exception("Failed to login due to a throwable of type " + e.getClass().getName(), e); } this.loggedIn = true; return this.subject; } /** * Logs out from the server and disconnects this client. */ public void logout() { if (this.loggedIn && this.subject != null) { try { Method logoutMethod = SubjectManagerRemote.class.getDeclaredMethod("logout", Subject.class); remoteInvoke(RhqManager.SubjectManager, logoutMethod, Void.class, this.subject); } catch (NoSuchMethodException e) { throw new IllegalStateException( "Couldn't find the logout method on the SubjectManagerRemote interface.", e); } catch (Throwable e) { // just keep going so we can disconnect this client } } doDisconnect(); this.subject = null; this.loggedIn = false; } /** * Connects to the remote server but does not establish a user session. This can be used * with the limited API that does not require a Subject. * * After successfully executing this, {@link #isConnected()} will be <code>true</code> * and {@link #getSubject()} will return the subject that this method returns. * @throws Exception if failed to connect to the server or log in */ public void connect() throws Exception { if (this.loggedIn) { String name = (null == this.subject) ? "" : this.subject.getName(); throw new IllegalStateException("User " + name + " must log out before connection can be established."); } doDisconnect(); doConnect(); this.connected = true; } /** * Disconnect from the server. */ public void disconnect() { if (this.loggedIn) { String name = (null == this.subject) ? "" : this.subject.getName(); throw new IllegalStateException("User " + name + " is logged in. Call logout() instead of disconnect()."); } doDisconnect(); this.connected = false; } /** * Returns <code>true</code> if and only if this client successfully connected * to the remote server and the user successfully logged in. * * @return if the user was able to connect and log into the server */ public boolean isLoggedIn() { return this.loggedIn; } /** * Returns <code>true</code> if and only if this client successfully connected * to the remote server. * * @return if the user was able to connect and log into the server */ public boolean isConnected() { return this.connected; } /** * Returns the information on the user that is logged in. * May be <code>null</code> if the user never logged in successfully. * * @return user information or <code>null</code> */ public Subject getSubject() { return this.subject; } public URI getRemoteURI() { try { return new URI(getTransport(), null, getHost(), getPort(), null, null, null); } catch (URISyntaxException e) { //does not happen, but hey LOG.error("Error creating the remote URI with transport, host and port: " + getTransport() + ", " + getHost() + " and " + getPort(), e); return null; } } public String getHost() { return this.host; } public int getPort() { return this.port; } public String getTransport() { return transport; } protected String guessTransport(int port) { return String.valueOf(port).endsWith("443") ? SECURE_TRANSPORT : NONSECURE_TRANSPORT; } /** * Sets the underlying transport to use to communicate with the server. * Available transports are "servlet" and "sslservlet". * If you set it to <code>null</code>, then the transport to be used will * be set appropriately for the {@link #getPort()} (e.g. a secure transport * will be used for ports that end with 443, a non-secure transport will be * used for all other ports). * * @param transport */ public void setTransport(String transport) { this.transport = transport; } /** * Returns the map of all remote managers running in the server that this * client can talk to. * * @return Map K=manager name V=remote proxy */ public Map<RhqManager, Object> getScriptingAPI() { if (this.managers == null) { this.managers = new HashMap<RhqManager, Object>(); for (RhqManager manager : RhqManager.values()) { if (manager.enabled()) { try { Object proxy = getProcessor(this, manager, true); this.managers.put(manager, proxy); } catch (Throwable e) { LOG.error("Failed to load manager " + manager + " due to missing class.", e); } } } } return this.managers; } @Override public <T> T getProxy(Class<T> remoteApiIface) { RhqManager manager = RhqManager.forInterface(remoteApiIface); if (manager == null) { throw new IllegalArgumentException("Unknown remote interface " + remoteApiIface); } return getProcessor(this, manager, false); } @Override public String toString() { return this.getClass().getSimpleName() + "[" + "transport=" + transport + ", host=" + host + ", port=" + port + ", subsystem=" + subsystem + ", connected=" + connected + ", loggedIn=" + loggedIn + ", subject=" + subject + ']'; } /** * Returns the internal JBoss/Remoting client used to perform the low-level * comm with the server. * * This is package-scoped so the proxy can use it. * * @return remoting client used to talk to the server */ Client getRemotingClient() { return this.remotingClient; } /** * If the client is connected, this is version of the server that the client is talking to. * * @return remote server version */ public String getServerVersion() { return (this.serverInfo != null) ? this.serverInfo.getVersion() : null; } /** * If the client is connected, this is update version of the server that the client is talking to. Ex. Update 02 * * @return remote server version */ public String getServerVersionUpdate() { return (this.serverInfo != null) ? this.serverInfo.getVersionUpdate() : null; } /** * If the client is connected, this is build number of the server that the client is talking to. * * @return remote server build number */ public String getServerBuildNumber() { return (this.serverInfo != null) ? this.serverInfo.getBuildNumber() : null; } @SuppressWarnings("unchecked") private static <T> T getProcessor(RemoteClient remoteClient, RhqManager manager, boolean simplify) { try { RemoteClientProxy gpc = new RemoteClientProxy(remoteClient, manager); Class<?> intf = simplify ? InterfaceSimplifier.simplify(manager.remote()) : manager.remote(); return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[] { intf }, gpc); } catch (Exception e) { throw new RuntimeException("Failed to get remote connection proxy", e); } } private void doDisconnect() { try { if (this.remotingClient != null && this.remotingClient.isConnected()) { this.remotingClient.disconnect(); } } catch (Exception e) { LOG.warn(e); // TODO what to do here? } finally { this.remotingClient = null; this.serverInfo = null; } } private void doConnect() throws Exception { String locatorURI = this.transport + "://" + this.host + ":" + this.port + "/jboss-remoting-servlet-invoker/ServerInvokerServlet"; InvokerLocator locator = new InvokerLocator(locatorURI); String subsystem = "REMOTEAPI"; if ((this.subsystem != null) && (this.subsystem.trim().equalsIgnoreCase("WSREMOTEAPI"))) { subsystem = "WSREMOTEAPI"; } Map<String, String> remotingConfig = buildRemotingConfig(locatorURI); this.remotingClient = new Client(locator, subsystem, remotingConfig); this.remotingClient.connect(); // make sure the remote server can support this client try { Method getProductInfoMethod = SystemManagerRemote.class.getDeclaredMethod("getProductInfo", Subject.class); this.serverInfo = remoteInvoke(RhqManager.SystemManager, getProductInfoMethod, ProductInfo.class, this.subject); checkServerSupported(this.serverInfo); } catch (NoSuchMethodException e) { throw new IllegalStateException("Could not find the getProductInfo(Subject) method on the SystemManager.", e); } catch (Exception e) { // our client cannot be supported by the server - disconnect and rethrow the exception doDisconnect(); throw e; } catch (Throwable e) { throw new IllegalStateException("Unknown error occured during connect.", e); } } private Map<String, String> buildRemotingConfig(String locatorURI) { Map<String, String> config = new HashMap<String, String>(); if (SecurityUtil.isTransportSecure(locatorURI)) { setConfigProp(config, SSLSocketBuilder.REMOTING_KEY_STORE_FILE_PATH, "data/keystore.dat"); setConfigProp(config, SSLSocketBuilder.REMOTING_KEY_STORE_ALGORITHM, (System.getProperty("java.vendor", "") .contains("IBM") ? "IbmX509" : "SunX509")); setConfigProp(config, SSLSocketBuilder.REMOTING_KEY_STORE_TYPE, "JKS"); setConfigProp(config, SSLSocketBuilder.REMOTING_KEY_STORE_PASSWORD, "password"); setConfigProp(config, SSLSocketBuilder.REMOTING_KEY_PASSWORD, "password"); setConfigProp(config, SSLSocketBuilder.REMOTING_TRUST_STORE_FILE_PATH, null); setConfigProp(config, SSLSocketBuilder.REMOTING_TRUST_STORE_ALGORITHM, null); setConfigProp(config, SSLSocketBuilder.REMOTING_TRUST_STORE_TYPE, null); setConfigProp(config, SSLSocketBuilder.REMOTING_TRUST_STORE_PASSWORD, null); setConfigProp(config, SSLSocketBuilder.REMOTING_SSL_PROTOCOL, null); setConfigProp(config, SSLSocketBuilder.REMOTING_KEY_ALIAS, "self"); setConfigProp(config, SSLSocketBuilder.REMOTING_SERVER_AUTH_MODE, "false"); config.put(SSLSocketBuilder.REMOTING_SOCKET_USE_CLIENT_MODE, "true"); // since we do not know the server's client-auth mode, assume we need a keystore and let's make sure we have one SSLSocketBuilder dummy_sslbuilder = new SSLSocketBuilder(); // just so we can test finding our keystore try { // this allows the configured keystore file to be a URL, file path or a resource relative to our classloader dummy_sslbuilder.setKeyStoreURL(config.get(SSLSocketBuilder.REMOTING_KEY_STORE_FILE_PATH)); } catch (Exception e) { // this probably is due to the fact that the keystore doesn't exist yet - let's prepare one now SecurityUtil.createKeyStore(config.get(SSLSocketBuilder.REMOTING_KEY_STORE_FILE_PATH), config.get(SSLSocketBuilder.REMOTING_KEY_ALIAS), "CN=RHQ, OU=RedHat, O=redhat.com, C=US", config.get(SSLSocketBuilder.REMOTING_KEY_STORE_PASSWORD), config.get(SSLSocketBuilder.REMOTING_KEY_PASSWORD), "DSA", 36500); // now try to set it again, if an exception is still thrown, it's an unrecoverable error dummy_sslbuilder.setKeyStoreURL(config.get(SSLSocketBuilder.REMOTING_KEY_STORE_FILE_PATH)); } // in case the transport floats over https - we want to make sure a hostname verifier is installed and allows all hosts config.put(HTTPSClientInvoker.IGNORE_HTTPS_HOST, "true"); } return config; } /** * Looks up the prop name in system properties and puts the value in the map. If the property * isn't set, the given default is used. If the given default is null and the property isn't * set, then the map is not populated. * * @param configMap * @param propName * @param defaultValue */ private void setConfigProp(Map<String, String> configMap, String propName, String defaultValue) { String propValue = System.getProperty(propName, defaultValue); if (propValue != null) { configMap.put(propName, propValue); } return; } /** * Checks to see if the server (whose version information is passed in) supports this client. * This performs version checks and throws an IllegalStateException if the server does not * support this client. * * @param serverVersionInfo the information about the remote server * * @throws IllegalStateException if the remote server does not support this client */ private void checkServerSupported(ProductInfo serverVersionInfo) throws IllegalStateException { boolean supported; String serverVersionString; final String propName = "rhq.client.version-check"; final String versionCheckProp = System.getProperty(propName, "true"); if (!versionCheckProp.equalsIgnoreCase("true")) { return; } String clientVersionString = System.getProperty("rhq.client.version", null); try { if (clientVersionString == null) { clientVersionString = getClass().getPackage().getImplementationVersion(); } if (clientVersionString == null) { clientVersionString = " undefined "; } serverVersionString = this.serverInfo.getVersion(); ComparableVersion clientVersion = new ComparableVersion(clientVersionString); ComparableVersion serverVersion = new ComparableVersion(serverVersionString); int laterVersionCheck = clientVersion.compareTo(serverVersion); if (laterVersionCheck >= 0) { supported = true; //Ex. 3.2.0.GA-redhat-N represent supported non-breaking api patches/changes. } else { supported = false; } } catch (Exception e) { throw new IllegalStateException("Cannot determine if server version is supported.", e); // assume we can't talk to it } if (!supported) { String errMsg = "This client [" + clientVersionString + "] does not support the remote server [" + serverVersionString + "]"; throw new IllegalStateException(errMsg); } } }