/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wps.remote.plugin; import java.io.FileInputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.KeyStore; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLContext; import org.apache.commons.io.IOUtils; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.ssl.SSLContexts; import org.geoserver.ows.Dispatcher; import org.geoserver.ows.Request; import org.geoserver.ows.URLMangler.URLType; import org.geoserver.ows.util.RequestUtils; import org.geoserver.ows.util.ResponseUtils; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.platform.GeoServerResourceLoader; import org.geoserver.platform.resource.Resources; import org.geoserver.wps.process.RawData; import org.geoserver.wps.process.ResourceRawData; import org.geoserver.wps.process.StreamRawData; import org.geoserver.wps.process.StringRawData; import org.geoserver.wps.remote.RemoteMachineDescriptor; import org.geoserver.wps.remote.RemoteProcessClient; import org.geoserver.wps.remote.RemoteProcessClientListener; import org.geoserver.wps.remote.RemoteProcessFactoryConfiguration; import org.geoserver.wps.remote.RemoteProcessFactoryConfigurationWatcher; import org.geoserver.wps.remote.RemoteProcessFactoryListener; import org.geoserver.wps.remote.RemoteRequestDescriptor; import org.geotools.feature.NameImpl; import org.geotools.util.logging.Logging; import org.jivesoftware.smack.Chat; import org.jivesoftware.smack.ChatManager; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.MessageListener; import org.jivesoftware.smack.PacketCollector; import org.jivesoftware.smack.PacketListener; import org.jivesoftware.smack.Roster; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.bosh.BOSHConfiguration; import org.jivesoftware.smack.bosh.XMPPBOSHConnection; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.filter.PacketIDFilter; import org.jivesoftware.smack.filter.PacketTypeFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Presence.Type; import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.muc.DiscussionHistory; import org.jivesoftware.smackx.muc.MultiUserChat; import org.opengis.feature.type.Name; import org.opengis.util.ProgressListener; import net.razorvine.pickle.Opcodes; import net.razorvine.pickle.PickleException; import net.razorvine.pickle.PickleUtils; import net.razorvine.pickle.Pickler; import net.razorvine.pickle.Unpickler; /** * XMPP implementation of the {@link RemoteProcessClient} * * @author Alessio Fabiani, GeoSolutions * */ public class XMPPClient extends RemoteProcessClient { /** The LOGGER */ public static final Logger LOGGER = Logging.getLogger(XMPPClient.class.getPackage().getName()); private static final int DEFAULT_PACKET_REPLY_TIMEOUT = 500; // millis /** The XMPP Server endpoint */ private String server; /** The XMPP Server port */ private int port; /** * XMPP specific parameters and properties */ private XMPPConnection connection; private ConnectionConfiguration config; private ChatManager chatManager; private PacketListener packetListener; private ServiceDiscoveryManager discoStu; private Map<String, Chat> openChat = Collections.synchronizedMap(new HashMap<String, Chat>()); private String domain; private String bus; private String managementChannelUser; private String managementChannelPassword; protected String managementChannel; /** * Private structures */ protected List<String> serviceChannels; /* * protected Map<String, List<String>> occupantsList = Collections .synchronizedMap(new HashMap<String, List<String>>()); */ protected List<Name> registeredServices = Collections.synchronizedList(new ArrayList<Name>()); protected List<MultiUserChat> mucServiceChannels = new ArrayList<MultiUserChat>(); protected MultiUserChat mucManagementChannel; /** Primitive type name -> class map. */ public static final Map<String, Object> PRIMITIVE_NAME_TYPE_MAP = new HashMap<String, Object>(); /** Default Thresholds indicating overloaded resources */ private static double DEFAULT_CPU_PERCENT_THRESHOLD = 82.0; private static double DEFAULT_MEM_PERCENT_THRESHOLD = 82.0; /** Setup the primitives map. */ static enum CType { SIMPLE, COMPLEX } /** * * STATIC MAP of the available mime-types. * * Those are the available key-strigns which the remote client can specify on the XMPP message in order to declare which kind of output objects it * is able to produce. * * */ static { // ---- PRIMITIVE_NAME_TYPE_MAP.put("string", new Object[] { String.class, CType.SIMPLE, null, "text/plain" }); PRIMITIVE_NAME_TYPE_MAP.put("url", new Object[] { String.class, CType.SIMPLE, null, "text/plain" }); PRIMITIVE_NAME_TYPE_MAP.put("boolean", new Object[] { Boolean.TYPE, CType.SIMPLE, Boolean.TRUE, "" }); PRIMITIVE_NAME_TYPE_MAP.put("byte", new Object[] { Byte.TYPE, CType.SIMPLE, null, "" }); PRIMITIVE_NAME_TYPE_MAP.put("char", new Object[] { Character.TYPE, CType.SIMPLE, null, "text/plain" }); PRIMITIVE_NAME_TYPE_MAP.put("short", new Object[] { Short.TYPE, CType.SIMPLE, null, "" }); PRIMITIVE_NAME_TYPE_MAP.put("int", new Object[] { Integer.TYPE, CType.SIMPLE, null, "" }); PRIMITIVE_NAME_TYPE_MAP.put("long", new Object[] { Long.TYPE, CType.SIMPLE, null, "" }); PRIMITIVE_NAME_TYPE_MAP.put("float", new Object[] { Float.TYPE, CType.SIMPLE, null, "" }); PRIMITIVE_NAME_TYPE_MAP.put("double", new Object[] { Double.TYPE, CType.SIMPLE, null, "" }); PRIMITIVE_NAME_TYPE_MAP.put("datetime", new Object[] { Date.class, CType.SIMPLE, null, "" }); // Complex and Raw data types // ---- PRIMITIVE_NAME_TYPE_MAP.put("application/xml", new Object[] { RawData.class, CType.COMPLEX, new StringRawData("", "application/xml"), "application/xml,text/xml", ".xml" }); PRIMITIVE_NAME_TYPE_MAP.put("text/xml", new Object[] { RawData.class, CType.COMPLEX, new StringRawData("", "text/xml"), "application/xml,text/xml", ".xml" }); // ---- PRIMITIVE_NAME_TYPE_MAP.put("text/xml;subtype", new Object[] { RawData.class, CType.COMPLEX, new StringRawData("", "application/gml-3.1.1"), "application/xml,application/gml-3.1.1,application/gml-2.1.2,text/xml; subtype=gml/3.1.1,text/xml; subtype=gml/2.1.2", ".xml" }); PRIMITIVE_NAME_TYPE_MAP.put("text/xml;subtype=gml/3.1.1", PRIMITIVE_NAME_TYPE_MAP.get("text/xml;subtype")); PRIMITIVE_NAME_TYPE_MAP.put("text/xml;subtype=gml/2.1.2", PRIMITIVE_NAME_TYPE_MAP.get("text/xml;subtype")); PRIMITIVE_NAME_TYPE_MAP.put("application/gml-3.1.1", PRIMITIVE_NAME_TYPE_MAP.get("text/xml;subtype")); PRIMITIVE_NAME_TYPE_MAP.put("application/gml-2.1.2", PRIMITIVE_NAME_TYPE_MAP.get("text/xml;subtype")); // ---- PRIMITIVE_NAME_TYPE_MAP.put("application/json", new Object[] { RawData.class, CType.COMPLEX, new StringRawData("", "application/vnd.geo+json"), "application/vnd.geo+json", ".json" }); // ---- PRIMITIVE_NAME_TYPE_MAP.put("application/owc", new Object[] { RawData.class, CType.COMPLEX, new StringRawData("", "application/vnd.geo+json"), "application/vnd.geo+json", ".json" }); // ---- PRIMITIVE_NAME_TYPE_MAP.put("image/geotiff", new Object[] { RawData.class, CType.COMPLEX, new ResourceRawData(null, "image/geotiff", "tif"), "image/geotiff,image/tiff", ".tif" }); PRIMITIVE_NAME_TYPE_MAP.put("image/geotiff;stream", new Object[] { RawData.class, CType.COMPLEX, new StreamRawData("image/geotiff", null, "tif"), "image/geotiff,image/tiff", ".tif" }); // ---- PRIMITIVE_NAME_TYPE_MAP.put("application/zip", new Object[] { RawData.class, CType.COMPLEX, new ResourceRawData(null, "application/zip", "zip"), "application/zip", ".zip" }); PRIMITIVE_NAME_TYPE_MAP.put("application/zip;stream", new Object[] { RawData.class, CType.COMPLEX, new StreamRawData("application/zip", null, "zip"), "application/zip", ".zip" }); // ---- PRIMITIVE_NAME_TYPE_MAP.put("application/x-netcdf", new Object[] { RawData.class, CType.COMPLEX, new ResourceRawData(null, "application/x-netcdf", "nc"), "application/x-netcdf", ".nc" }); PRIMITIVE_NAME_TYPE_MAP.put("application/x-netcdf;stream", new Object[] { RawData.class, CType.COMPLEX, new StreamRawData("application/x-netcdf", null, "nc"), "application/x-netcdf", ".nc" }); // ---- PRIMITIVE_NAME_TYPE_MAP.put("video/mp4", new Object[] { RawData.class, CType.COMPLEX, new ResourceRawData(null, "video/mp4", "mp4"), "video/mp4", ".mp4" }); PRIMITIVE_NAME_TYPE_MAP.put("video/mp4;stream", new Object[] { RawData.class, CType.COMPLEX, new StreamRawData("video/mp4", null, "mp4"), "video/mp4", ".mp4" }); } /** * Default Constructor * * @param remoteProcessFactoryConfigurationWatcher * @param enabled */ public XMPPClient( RemoteProcessFactoryConfigurationWatcher remoteProcessFactoryConfigurationWatcher, boolean enabled, int priority) { super(remoteProcessFactoryConfigurationWatcher, enabled, priority); this.server = getConfiguration().get("xmpp_server"); this.port = Integer.parseInt(getConfiguration().get("xmpp_port")); this.domain = getConfiguration().get("xmpp_domain"); this.bus = getConfiguration().get("xmpp_bus"); this.managementChannelUser = getConfiguration().get("xmpp_management_channel_user"); this.managementChannelPassword = getConfiguration().get("xmpp_management_channel_pwd"); this.managementChannel = getConfiguration().get("xmpp_management_channel"); this.serviceChannels = new ArrayList<String>(); String[] serviceNamespaces = getConfiguration().get("xmpp_service_channels").split(","); for (int sc = 0; sc < serviceNamespaces.length; sc++) { this.serviceChannels.add(serviceNamespaces[sc].trim()); } } @Override public void init() throws Exception { // Initializes the XMPP Client and starts the communication. It also // register GeoServer as "manager" to the service channels on the MUC // (Multi // User Channel) Rooms LOGGER.info( String.format("Initializing connection to server %1$s port %2$d", server, port)); int packetReplyTimeout = DEFAULT_PACKET_REPLY_TIMEOUT; if (getConfiguration().get("xmpp_packet_reply_timeout") != null) { packetReplyTimeout = Integer .parseInt(getConfiguration().get("xmpp_packet_reply_timeout")); } SmackConfiguration.setDefaultPacketReplyTimeout(packetReplyTimeout); config = new ConnectionConfiguration(server, port); checkSecured(getConfiguration()); // Trust own CA and all self-signed certs SSLContext sslcontext = null; if (this.certificateFile != null && this.certificatePassword != null) { KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); FileInputStream instream = new FileInputStream(this.certificateFile); try { trustStore.load(instream, this.certificatePassword.toCharArray()); } finally { instream.close(); } sslcontext = SSLContexts.custom() .loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()).build(); } if (sslcontext != null) { // config.setSASLAuthenticationEnabled(false); config.setSecurityMode(SecurityMode.enabled); config.setCustomSSLContext(sslcontext); } else { config.setSecurityMode(SecurityMode.disabled); } // Actually performs the connection to the XMPP Server for (int testConn=0; testConn<5; testConn++) { try { // Try first the TCP Endpoint connection = new XMPPTCPConnection(config); connection.connect(); break; } catch(NoResponseException e) { connection = null; if (testConn >= 5) { LOGGER.warning("No XMPP TCP Endpoint available or could not get any response from the Server. Falling back to BOSH Endpoint."); } else { LOGGER.log(Level.WARNING, "Tentative #" + (testConn+1) + " - Error while trying to connect to XMPP TCP Endpoint.", e); Thread.sleep(500); } } } if (connection == null || !connection.isConnected()) { for (int testConn=0; testConn<5; testConn++) { try { // Falling back to BOSH Endpoint BOSHConfiguration boshConfig = new BOSHConfiguration((sslcontext != null), server, port, null, getConfiguration().get("xmpp_domain")); if (sslcontext != null) { // boshConfig.setSASLAuthenticationEnabled(false); boshConfig.setSecurityMode(SecurityMode.enabled); boshConfig.setCustomSSLContext(sslcontext); } else { boshConfig.setSecurityMode(SecurityMode.disabled); } connection = new XMPPBOSHConnection(boshConfig); connection.connect(); break; } catch(NoResponseException e) { connection = null; if (testConn >= 5) { LOGGER.warning("No XMPP BOSH Endpoint available or could not get any response from the Server. The XMPP Client won't be available."); } else { LOGGER.log(Level.WARNING, "Tentative #" + (testConn+1) + " - Error while trying to connect to XMPP BOSH Endpoint.", e); Thread.sleep(500); } } } } LOGGER.info("Connected: " + connection.isConnected()); // Check if the connection to the XMPP server is successful; the login // and registration is not yet performed at this time if (connection.isConnected()) { chatManager = ChatManager.getInstanceFor(connection); discoStu = ServiceDiscoveryManager.getInstanceFor(connection); // Add features to our XMPP client discoProperties(); // Performs login with "admin" user credentials performLogin(getConfiguration().get("xmpp_manager_username"), getConfiguration().get("xmpp_manager_password")); // Start "ping" task in order to maintain alive the connection startPingTask(); // Send invitation to the registered endpoints sendInvitations(); // getEndpointsLoadAverages(); // checkPendingRequests(); } else { setEnabled(false); LOGGER.warning("Not connected! The XMPP client has been disabled."); } } private void checkSecured(RemoteProcessFactoryConfiguration configuration) { final String xmppServerEmbeddedSecure = configuration.get("xmpp_server_embedded_secure"); final String xmppServerEmbeddedCertFile = configuration.get("xmpp_server_embedded_certificate_file"); final String xmppServerEmbeddedCertPwd = configuration.get("xmpp_server_embedded_certificate_password"); if (xmppServerEmbeddedSecure != null && Boolean.valueOf(xmppServerEmbeddedSecure.trim())) { // Override XML properties if (xmppServerEmbeddedCertFile != null && xmppServerEmbeddedCertPwd != null) { final org.geoserver.platform.resource.Resource certFileResource = Resources.fromURL(xmppServerEmbeddedCertFile.trim()); if (certFileResource != null) { this.certificateFile = certFileResource.file(); this.certificatePassword = xmppServerEmbeddedCertPwd.trim(); } else { // Get the Resource loader GeoServerResourceLoader loader = GeoServerExtensions.bean(GeoServerResourceLoader.class); try { // Copy the default property file into the data directory //URL url = RemoteProcessFactoryConfigurationWatcher.class.getResource(xmppServerEmbeddedCertFile.trim()); //if (url != null) { this.certificateFile = loader.createFile(xmppServerEmbeddedCertFile.trim()); loader.copyFromClassPath(xmppServerEmbeddedCertFile.trim(), this.certificateFile/*, RemoteProcessFactoryConfigurationWatcher.class*/); //} this.certificateFile = loader.find(xmppServerEmbeddedCertFile.trim()); this.certificatePassword = xmppServerEmbeddedCertPwd.trim(); } catch (IOException e) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, e.getMessage(), e); } } } } } } @Override public String execute(Name serviceName, Map<String, Object> input, Map<String, Object> metadata, ProgressListener monitor) throws Exception { // Check for a free machine... LOGGER.info("XMPPClient::execute - searching available remote process machine for service [" + serviceName + "]"); /** * Sanity Checks */ if (metadata == null) { throw new Exception("Could not send a Request Message to the Remote XMPP Client!"); } /** * Collecting Request Info and Inputs */ // Extract the process inputs final Object fixedInputs = getFixedInputs(input); // Generate a unique pID to be used to identify the endpoint final String pid = md5Java(serviceName.getNamespaceURI() + "." + serviceName.getLocalPart() + System.nanoTime() + byteArrayToURLString(pickle(fixedInputs))); // Try to retrieve the base URL from the request and send to the // endpoint as parameter Request request = Dispatcher.REQUEST.get(); metadata.put("request", request); String baseURL = getGeoServer().getGlobal().getSettings().getProxyBaseUrl(); try { if (baseURL == null) { baseURL = RequestUtils.baseURL(request.getHttpRequest()); } baseURL = ResponseUtils.buildURL(baseURL, "/", null, URLType.SERVICE); } catch (Exception e) { LOGGER.warning("Could not acquire the GeoServer Base URL!"); } // Build and send the REUQEST message /** * topic = request id = pid baseURL = geoserver url message = <pickled WPS inputs> */ final String msg = "topic=request&id=" + pid + "&baseURL=" + baseURL + "&message=" + byteArrayToURLString(pickle(fixedInputs)); /** * Looking for an available remote processing node */ final String serviceJID = getFlattestMachine(serviceName); if (serviceJID != null) { /** * We have a JID to an available processing node; send the request message to it... */ // Update Metadata metadata.put("serviceJID", serviceJID); LOGGER.info("XMPPClient::execute - extracting the PID for the service JID [" + serviceJID + "] with inputs [" + fixedInputs + "]"); sendMessage(serviceJID, msg); } else { /** * We could not find a suitable processing node to serve the request; queue it for later checks... */ pendingRequests .add(new RemoteRequestDescriptor(serviceName, input, metadata, pid, baseURL)); } return pid; } /** * Utility method to extract the process inputs accordingly to whatever declared from the endpoint. * * @param input * * @throws IOException */ private Object getFixedInputs(Map<String, Object> input) throws IOException { Map<String, Object> fixedInputs = new HashMap<String, Object>(); for (Entry<String, Object> entry : input.entrySet()) { final String key = entry.getKey(); final Object value = entry.getValue(); Object fixedValue = value; if (value instanceof RawData) { fixedValue = IOUtils.toString(((RawData) value).getInputStream(), "UTF-8"); } else if (value instanceof List) { List<Object> values = (List<Object>) value; if (values != null && values.size() > 0 && values.get(0) instanceof RawData) { fixedValue = new ArrayList<String>(); for (Object o : values) { ((List<String>) fixedValue) .add(IOUtils.toString(((RawData) o).getInputStream(), "UTF-8")); } } } fixedInputs.put(key, fixedValue); } return fixedInputs; } /** * Add features to our XMPP client We do support Data forms, XHTML-IM, Service Discovery */ private void discoProperties() { discoStu.addFeature("http://jabber.org/protocol/xhtml-im"); discoStu.addFeature("jabber:x:data"); discoStu.addFeature("http://jabber.org/protocol/disco#info"); discoStu.addFeature("jabber:iq:privacy"); discoStu.addFeature("http://jabber.org/protocol/si"); discoStu.addFeature("http://jabber.org/protocol/bytestreams"); discoStu.addFeature("http://jabber.org/protocol/ibb"); } /** * Logins as manager to the XMPP Server and registers to the service channels management chat rooms * * @param username * @param password */ public void performLogin(String username, String password) throws Exception { if (connection != null && connection.isConnected()) { connection.login(username, password, getResource(username)); // Create a MultiUserChat using a XMPPConnection for a room // User joins the new room using a password and specifying // the amount of history to receive. In this example we are // requesting the last 5 messages. DiscussionHistory history = new DiscussionHistory(); history.setMaxStanzas(5); mucManagementChannel = new MultiUserChat(connection, managementChannel + "@" + bus + "." + domain); try { mucManagementChannel.join(getJID(username), managementChannelPassword); /* * , history, connection. getPacketReplyTimeout()); */ } catch (Exception e) { mucManagementChannel.join(username, managementChannelPassword); } for (String channel : serviceChannels) { MultiUserChat serviceChannel = new MultiUserChat(connection, channel + "@" + bus + "." + domain); try { serviceChannel.join(getJID(username), managementChannelPassword); /* * , history, connection. getPacketReplyTimeout()); */ } catch (Exception e) { serviceChannel.join(username, managementChannelPassword); } mucServiceChannels.add(serviceChannel); } // setStatus(true, "Orchestrator Active"); // setupListeners(); } } /** * Generate the XMPP JID * * @param username * */ private String getJID(String username) { // final String id = md5Java(username + "@" + this.domain + "/" + System.nanoTime()); return username + "@" + this.domain; } /** * Generate a unique Server JID Resource * * @param username * */ private String getResource(String username) { final String id = md5Java(username + "@" + this.domain + "/" + System.nanoTime()); try { return /* this.domain + "/" + */id + "@" + InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { return /* this.domain + "/" + */id + "@geoserver"; } } /** * Declare the status on the XMPP Chat * * @param available * @param status */ public void setStatus(boolean available, String status) throws Exception { Presence.Type type = available ? Type.available : Type.unavailable; Presence presence = new Presence(type); presence.setStatus(status); connection.sendPacket(presence); } /** * Destroy the connection */ public void destroy() throws Exception { if (connection != null && connection.isConnected()) { stopPingTask(); connection.disconnect(); } } /** * * * @param user * @param name */ public void createEntry(String user, String name) throws Exception { LOGGER.fine(String.format("Creating entry for buddy '%1$s' with name %2$s", user, name)); Roster roster = connection.getRoster(); roster.createEntry(user, name, null); } /** * This handles the chat listener. We can't simply listen to chats for some reason, and instead have to grab the chats from the packets. The other * listeners work properly in SMACK */ public void setupListeners() { /* * This is the actual code that handles what happens with XMPP users */ packetListener = new XMPPPacketListener(this); // PacketFilter filter = new MessageTypeFilter(Message.Type.chat); connection.addPacketListener(packetListener, null); } /** * Conversation setup! * * Messages should be moved here once we get this working properly */ public Chat setupChat(final String origin) { synchronized (openChat) { if (openChat.get(origin) != null) { return openChat.get(origin); } MessageListener listener = new MessageListener() { public void processMessage(Chat chat, Message message) { // TODO: Fix this so that this actually does something! } }; Chat chat = chatManager.createChat(origin, listener); openChat.put(origin, chat); return chat; } } /** * This is the code that handles HTML messages */ public void sendMessage(String person, String message) { synchronized (openChat) { Chat chat = openChat.get(person); if (chat == null) chat = setupChat(person); try { chat.sendMessage(message); } catch (XMPPException e) { LOGGER.log(Level.SEVERE, "xmppClient._ReceiveError", e); } catch (NotConnectedException e) { LOGGER.log(Level.SEVERE, "xmppClient._ReceiveError", e); } } } /** * Close the XMPP connection * * @throws NotConnectedException */ public void disconnect() throws NotConnectedException { connection.disconnect(); } /** * Utility method to extract the Service Name from the XMPP JID * * @param person * * */ public static NameImpl extractServiceName(String person) throws Exception { String occupantFlatName = null; if (person.lastIndexOf("@") < person.indexOf("/")) { occupantFlatName = person.substring(person.indexOf("/") + 1); } else { occupantFlatName = person.substring(person.indexOf("/") + 1); occupantFlatName = occupantFlatName.substring(0, occupantFlatName.indexOf("@")); } if (occupantFlatName.indexOf(".") > 0) { final String serviceName[] = occupantFlatName.split("\\."); return new NameImpl(serviceName[0], serviceName[1]); } else { final String channel = person.substring(0, person.indexOf("@")); return new NameImpl(channel, occupantFlatName); } } /** * Send an invitation to the new logged in member * */ protected void sendInvitations() throws Exception { synchronized (registeredServices) { for (MultiUserChat mucServiceChannel : mucServiceChannels) { for (String occupant : mucServiceChannel.getOccupants()) { final Name serviceName = extractServiceName(occupant); // send invitation and register source JID String[] serviceJIDParts = occupant.split("/"); if (serviceJIDParts.length == 3 && (serviceJIDParts[2].startsWith("master") || serviceJIDParts[2].indexOf("@") < 0)) { sendMessage(occupant, "topic=invite"); } // register service on listeners if (!registeredServices.contains(serviceName)) { registeredServices.add(serviceName); } } } } } /** * Scan Remote Processing Machines availability and average load * * @throws Exception */ protected void getEndpointsLoadAverages() throws Exception { synchronized (registeredProcessingMachines) { List<String> nodeJIDs = new ArrayList<String>(); for (RemoteMachineDescriptor node : registeredProcessingMachines) { nodeJIDs.add(node.getNodeJID()); //node.setAvailable(false); } for (MultiUserChat mucServiceChannel : mucServiceChannels) { for (String occupant : mucServiceChannel.getOccupants()) { if (!nodeJIDs.contains(occupant)) { registeredProcessingMachines.add(new RemoteMachineDescriptor(occupant, extractServiceName(occupant), false, 0.0, 0.0)); } // send invitation and register source JID String[] serviceJIDParts = occupant.split("/"); if (serviceJIDParts.length == 3 && (serviceJIDParts[2].startsWith("master") || serviceJIDParts[2].indexOf("@") < 0)) { sendMessage(occupant, "topic=getloadavg"); } } } } } /** * Scan pending requests queue; try to find a free remote node suitable for processing or abort the request if expired. * * @throws Exception */ protected void checkPendingRequests() throws Exception { synchronized (pendingRequests) { for (RemoteRequestDescriptor request : pendingRequests) { // Check if the request is still valid final String pid = request.getPid(); boolean isRequestValid = false; for (RemoteProcessClientListener process : getRemoteClientListeners()) { if (process.getPID().equals(pid)) { isRequestValid = true; break; } } if (!isRequestValid) { // Remove the request from the queue pendingRequests.remove(request); continue; } // Check if the request can be executed by a remote processing node final Name serviceName = request.getServicename(); final String serviceJID = getFlattestMachine(serviceName); if (serviceJID != null) { // Extract the process inputs final Object fixedInputs = getFixedInputs(request.getInput()); // Build and send the REUQEST message /** * topic = request id = pid baseURL = geoserver url message = <pickled WPS inputs> */ final String msg = "topic=request&id=" + pid + "&baseURL=" + request.getBaseURL() + "&message=" + byteArrayToURLString(pickle(fixedInputs)); /** * We have a JID to an available processing node; send the request message to it... */ // Update Metadata request.getMetadata().put("serviceJID", serviceJID); LOGGER.info("XMPPClient::execute - extracting the PID for the service JID [" + serviceJID + "] with inputs [" + fixedInputs + "]"); sendMessage(serviceJID, msg); // Remove the request from the queue pendingRequests.remove(request); continue; } } } } /** * A new member joined one of the service chat-rooms; send an invitation and see if it is a remote service. If so, register it * * @param p * */ protected void handleMemberJoin(Presence p) throws Exception { synchronized (registeredServices) { LOGGER.finer("Member " + p.getFrom() + " joined the chat."); final Name serviceName = extractServiceName(p.getFrom()); // send invitation and register source JID String[] serviceJIDParts = p.getFrom().split("/"); if (serviceJIDParts.length == 3 && (serviceJIDParts[2].startsWith("master") || serviceJIDParts[2].indexOf("@") < 0)) { sendMessage(p.getFrom(), "topic=invite"); } if (!registeredServices.contains(serviceName)) { registeredServices.add(serviceName); } } } /** * A member leaved one of the service chat-rooms; lets remove the service declaration and de-register it * * @param p * */ protected void handleMemberLeave(Packet p) throws Exception { final Name serviceName = extractServiceName(p.getFrom()); LOGGER.finer("Member " + p.getFrom() + " leaved the chat."); if (registeredServices.contains(serviceName)) { registeredServices.remove(serviceName); } for (RemoteProcessFactoryListener listener : getRemoteFactoryListeners()) { listener.deregisterProcess(serviceName); } } /** * Find the service by name with the smallest amount of processes running, channel is decoded in service name * * e.g. debug.foo@bar/service@localhost * * @param service name * * @param candidateServiceJID * * */ private String getFlattestMachine(Name serviceName) { // The candidate remote processing node RemoteMachineDescriptor candidateNode = null; /** Thresholds indicating overloaded resources */ Double cpuLoadPercThreshold = DEFAULT_CPU_PERCENT_THRESHOLD; Double vmemPercThreshold = DEFAULT_MEM_PERCENT_THRESHOLD; if (getConfiguration().get("xmpp_cpu_perc_threshold") != null) { cpuLoadPercThreshold = Double.valueOf(getConfiguration().get("xmpp_cpu_perc_threshold")); } if (getConfiguration().get("xmpp_mem_perc_threshold") != null) { vmemPercThreshold = Double.valueOf(getConfiguration().get("xmpp_mem_perc_threshold")); } synchronized (registeredProcessingMachines) { LOGGER.info( "XMPPClient::getFlattestMachine - scanning the connected remote services..."); for (RemoteMachineDescriptor node : registeredProcessingMachines) { if (node.getAvailable() && node.getServiceName().equals(serviceName)) { if (node.getLoadAverage() >= cpuLoadPercThreshold || node.getMemPercUsed() >= vmemPercThreshold) { continue; } if (candidateNode == null || (node.getLoadAverage() <= candidateNode .getLoadAverage() && (node.getLoadAverage() < candidateNode.getLoadAverage() || node.getMemPercUsed() < candidateNode.getMemPercUsed()))) { candidateNode = node; } } } } // Return the candidate remote processing node JID or null if (candidateNode != null) { return candidateNode.getNodeJID(); } return null; } /** * Keep connection alive and check for network changes by sending ping packets */ Thread pingThread; private static int ping_task_generation = 1; void startPingTask() { // Schedule a ping task to run. PingTask task = new PingTask(); pingThread = new Thread(task); task.setThread(pingThread); pingThread.setDaemon(true); pingThread.setName("XmppConnection Pinger " + ping_task_generation); ping_task_generation++; pingThread.start(); } void stopPingTask() { pingThread = null; } class PingTask implements Runnable { private static final long DEFAULT_INITIAL_PING_DELAY = 20000; private static final long DEFAULT_PING_INTERVAL = 30000; private static final long DEFAULT_PING_TIMEOUT = 10000; private long delay; private long timeout; private long start_delay; private Thread thread; /** * */ public PingTask() { this.delay = DEFAULT_PING_INTERVAL; if (getConfiguration().get("xmpp_connection_ping_interval") != null) { this.delay = Long .parseLong(getConfiguration().get("xmpp_connection_ping_interval")); } this.timeout = DEFAULT_PING_TIMEOUT; if (getConfiguration().get("xmpp_connection_ping_timeout") != null) { this.timeout = Long .parseLong(getConfiguration().get("xmpp_connection_ping_timeout")); } this.start_delay = DEFAULT_INITIAL_PING_DELAY; if (getConfiguration().get("xmpp_connection_ping_initial_delay") != null) { this.start_delay = Long .parseLong(getConfiguration().get("xmpp_connection_ping_initial_delay")); } } /** * * @param thread */ protected void setThread(Thread thread) { this.thread = thread; } /** * * * @throws NotConnectedException */ private boolean sendPing() throws NotConnectedException { IQ req = new IQ() { public String getChildElementXML() { return "<ping xmlns='urn:xmpp:ping'/>"; } }; req.setType(IQ.Type.GET); PacketFilter filter = new AndFilter(new PacketIDFilter(req.getPacketID()), new PacketTypeFilter(IQ.class)); PacketCollector collector = connection.createPacketCollector(filter); connection.sendPacket(req); IQ result = (IQ) collector.nextResult(timeout); if (result == null) { LOGGER.warning("ping timeout"); return false; } collector.cancel(); return true; } /** * */ public void run() { try { // Sleep before sending first heartbeat. This will give time to // properly finish logging in. Thread.sleep(start_delay); } catch (InterruptedException ie) { // Do nothing } while (connection != null && pingThread == thread) { if (connection.isConnected() && connection.isAuthenticated()) { LOGGER.log(Level.FINER, "ping"); try { if (!sendPing()) { LOGGER.severe("ping failed - close connection"); try { connection.disconnect(); } catch (NotConnectedException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } else { // getEndpointsLoadAverages(); // checkPendingRequests(); } } catch (NotConnectedException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } catch (Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } else { // Try to reconnect... LOGGER.log(Level.FINER, "Try to reconnect..."); try { connection.connect(); LOGGER.info("Connected: " + connection.isConnected()); // check if the connection to the XMPP server is // successful; the login and registration is not yet // performed at this time if (connection.isConnected()) { chatManager = ChatManager.getInstanceFor(connection); discoStu = ServiceDiscoveryManager.getInstanceFor(connection); // discoProperties(); // performLogin(getConfiguration().get("xmpp_manager_username"), getConfiguration().get("xmpp_manager_password")); // startPingTask(); // sendInvitations(); // getEndpointsLoadAverages(); // checkPendingRequests(); } else { setEnabled(false); } } catch (Exception e) { LOGGER.log(Level.WARNING, "XMPP Could not reconnect!", e); } } try { // Sleep until we should write the next keep-alive. Thread.sleep(delay); } catch (InterruptedException ie) { // Do nothing } } LOGGER.log(Level.FINER, "pinger exit"); } } /** * Utility method to "pickle" (compress) the input parameters to be attached to the XMPP message * * @param unpickled * * @throws PickleException * @throws IOException */ static byte[] pickle(Object unpickled) throws PickleException, IOException { Pickler p = new Pickler(); return p.dumps(unpickled); } /** * Utility method to "un-pickle" (decompress) the input parameters attached to the XMPP message * * @param strdata * * @throws PickleException * @throws IOException */ static Object unPickle(String strdata) throws PickleException, IOException { return unPickle(PickleUtils.str2bytes(strdata)); } /** * Utility method to "un-pickle" (decompress) the input parameters attached to the XMPP message * * @param data * * @throws PickleException * @throws IOException */ static Object unPickle(byte[] data) throws PickleException, IOException { Unpickler u = new Unpickler(); Object o = u.loads(data); u.close(); return o; } /** * Utility method to get bytes out of a String * * @param s * * @throws IOException */ static byte[] toBytes(String s) throws IOException { try { byte[] bytes = PickleUtils.str2bytes(s); byte[] result = new byte[bytes.length + 3]; result[0] = (byte) Opcodes.PROTO; result[1] = 2; result[result.length - 1] = (byte) Opcodes.STOP; System.arraycopy(bytes, 0, result, 2, bytes.length); return result; } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } } /** * Utility method to get bytes out of a short array * * @param shorts * */ static byte[] toBytes(short[] shorts) { byte[] result = new byte[shorts.length + 3]; result[0] = (byte) Opcodes.PROTO; result[1] = 2; result[result.length - 1] = (byte) Opcodes.STOP; for (int i = 0; i < shorts.length; ++i) { result[i + 2] = (byte) shorts[i]; } return result; } /** * Utility method to generate a unique md5 * * @param message * */ public static String md5Java(String message) { String digest = null; try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] hash = md.digest(message.getBytes("UTF-8")); // converting byte array to Hexadecimal String StringBuilder sb = new StringBuilder(2 * hash.length); for (byte b : hash) { sb.append(String.format("%02x", b & 0xff)); } digest = sb.substring(0, 15).toString(); } catch (UnsupportedEncodingException ex) { LOGGER.log(Level.SEVERE, null, ex); } catch (NoSuchAlgorithmException ex) { LOGGER.log(Level.SEVERE, null, ex); } return digest; } /** * Convert a byte array to a URL encoded string * * @param in byte[] * @return String */ public static String byteArrayToURLString(byte in[]) { byte ch = 0x00; int i = 0; if (in == null || in.length <= 0) return null; String pseudo[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" }; StringBuffer out = new StringBuffer(in.length * 2); while (i < in.length) { // First check to see if we need ASCII or HEX if ((in[i] >= '0' && in[i] <= '9') || (in[i] >= 'a' && in[i] <= 'z') || (in[i] >= 'A' && in[i] <= 'Z') || in[i] == '$' || in[i] == '-' || in[i] == '_' || in[i] == '.' || in[i] == '!') { out.append((char) in[i]); i++; } else { out.append('%'); ch = (byte) (in[i] & 0xF0); // Strip off high nibble ch = (byte) (ch >>> 4); // shift the bits down ch = (byte) (ch & 0x0F); // must do this is high order bit is // on! out.append(pseudo[(int) ch]); // convert the nibble to a // String Character ch = (byte) (in[i] & 0x0F); // Strip off low nibble out.append(pseudo[(int) ch]); // convert the nibble to a // String Character i++; } } String rslt = new String(out); return rslt; } /** * Convert a list of Strings from an Interator into an array of Classes (the Strings are taken as classnames). * * @param it A java.util.Iterator pointing to a Collection of Strings * @param cl The ClassLoader to use * * @return Array of Classes * * @throws ClassNotFoundException When a class could not be loaded from the specified ClassLoader */ public final static Class<?>[] convertToJavaClasses(Iterator<String> it, ClassLoader cl) throws ClassNotFoundException { ArrayList<Class<?>> classes = new ArrayList<Class<?>>(); while (it.hasNext()) { classes.add(convertToJavaClass(it.next(), cl, null).getClazz()); } return classes.toArray(new Class[classes.size()]); } /** * Convert a given String into the appropriate Class. * * @param name Name of class * @param cl ClassLoader to use * @param object * @param sample * * @return The class for the given name * * @throws ClassNotFoundException When the class could not be found by the specified ClassLoader */ final static ParameterTemplate convertToJavaClass(String name, ClassLoader cl, Object defaultValue) throws ClassNotFoundException { int arraySize = 0; while (name.endsWith("[]")) { name = name.substring(0, name.length() - 2); arraySize++; } // Retrieve the Class of the parameter through the mapping String mimeTypes = ""; Class c = null; if (name.equalsIgnoreCase("complex") || name.equalsIgnoreCase("complex")) { // Is it a complex/raw data type? c = RawData.class; } else if (PRIMITIVE_NAME_TYPE_MAP.get(name) != null) { // Check for a primitive type c = (Class) ((Object[]) PRIMITIVE_NAME_TYPE_MAP.get(name))[0]; } if (c == null) { // No primitive, try to load it from the given ClassLoader try { c = cl.loadClass(name); } catch (ClassNotFoundException cnfe) { throw new ClassNotFoundException("Parameter class not found: " + name); } } // if we have an array get the array class if (arraySize > 0) { int[] dims = new int[arraySize]; for (int i = 0; i < arraySize; i++) { dims[i] = 1; } c = Array.newInstance(c, dims).getClass(); } // Set the default value or the sample object Object sample; if (defaultValue != null && CType.SIMPLE.equals(((Object[]) PRIMITIVE_NAME_TYPE_MAP.get(name))[1])) { sample = defaultValue; } else if (CType.COMPLEX.equals(((Object[]) PRIMITIVE_NAME_TYPE_MAP.get(name))[1]) && ((Object[]) PRIMITIVE_NAME_TYPE_MAP.get(name)).length > 2) { sample = ((Object[]) PRIMITIVE_NAME_TYPE_MAP.get(name))[2]; } else { sample = null; } if (PRIMITIVE_NAME_TYPE_MAP.get(name) != null) mimeTypes = (String) ((Object[]) PRIMITIVE_NAME_TYPE_MAP.get(name))[3]; return new ParameterTemplate(c, sample, mimeTypes); } } /** * Actual implementation of a "PacketListener". * * Listen to the service channels and handles the "registration" and "de-registration" of the available services (alias available WPS Processes) * * @author Alessio Fabiani, GeoSolutions * */ class XMPPPacketListener implements PacketListener { /** The LOGGER */ public static final Logger LOGGER = Logging .getLogger(XMPPPacketListener.class.getPackage().getName()); private XMPPClient xmppClient; public XMPPPacketListener(XMPPClient xmppClient) { this.xmppClient = xmppClient; } @Override public void processPacket(Packet packet) { if (packet instanceof Presence) { Presence p = (Presence) packet; try { if (p.isAvailable()) { if (p.getFrom().indexOf("@") > 0) { /** * Manage the channel occupants list */ final String channel = p.getFrom().substring(0, p.getFrom().indexOf("@")); /* * if (xmppClient.occupantsList.get(channel) == null) { xmppClient.occupantsList.put(channel, new ArrayList<String>()); } if * (xmppClient.occupantsList.get(channel) != null) { if (!xmppClient.occupantsList.get(channel).contains(p. getFrom())) * xmppClient.occupantsList.get(channel).add(p.getFrom() ); } */ if (xmppClient.serviceChannels.contains(channel)) xmppClient.handleMemberJoin(p); } } else if (!p.isAvailable()) { if (p.getFrom().indexOf("@") > 0 && p.getFrom().indexOf("/master") > 0) { boolean mustDeregisterService = true; final String channel = p.getFrom().substring(0, p.getFrom().indexOf("@")); final NameImpl serviceName = xmppClient.extractServiceName(p.getFrom()); for (MultiUserChat mucServiceChannel : xmppClient.mucServiceChannels) { if (mucServiceChannel.getRoom().startsWith(channel)) { for (String occupant : mucServiceChannel.getOccupants()) { if (!occupant.equals(p.getFrom())) { final Name occupantServiceName = xmppClient .extractServiceName(occupant); // send invitation and register source // JID String[] serviceJIDParts = occupant.split("/"); if (serviceJIDParts.length == 3 && (serviceJIDParts[2].startsWith("master") || serviceJIDParts[2].indexOf("@") < 0)) { if (serviceName.equals(occupantServiceName)) { mustDeregisterService = false; break; } } } } } } if (mustDeregisterService && xmppClient.serviceChannels.contains(channel)) xmppClient.handleMemberLeave(p); } } } catch (Exception e) { LOGGER.log(Level.WARNING, e.getMessage(), e); } } else if (packet instanceof Message) { Message message = (Message) packet; String origin = message.getFrom().split("/")[0]; Chat chat = xmppClient.setupChat(origin); if (message.getBody() != null) { LOGGER.fine("ReceivedMessage('" + message.getBody() + "','" + origin + "','" + message.getPacketID() + "');"); Map<String, String> signalArgs = new HashMap<String, String>(); try { String[] messageParts = message.getBody().split("&"); for (String mp : messageParts) { String[] signalArg = mp.split("="); signalArgs.put(signalArg[0], signalArg[1]); } } catch (Exception e) { LOGGER.log(Level.WARNING, "Wrong message! [" + message.getBody() + "]"); signalArgs.clear(); } if (!signalArgs.isEmpty() && signalArgs.containsKey("topic")) { for (XMPPMessage xmppMessage : GeoServerExtensions .extensions(XMPPMessage.class)) { if (xmppMessage.canHandle(signalArgs)) { xmppMessage.handleSignal(xmppClient, packet, message, signalArgs); } } } } } } } /** * Just an utility class helping us to convert a parameter to a Java class. * * @author Alessio Fabiani, GeoSolutions * */ class ParameterTemplate { private final Class<?> clazz; private final Object defaultValue; private final Map<String, String> meta = new HashMap<String, String>(); /** * @param clazz * @param defaultValue */ public ParameterTemplate(Class<?> clazz, Object defaultValue, String mimeTypes) { this.clazz = clazz; this.defaultValue = defaultValue; this.meta.put("mimeTypes", mimeTypes); if (mimeTypes != null && mimeTypes.split(",").length>0) { this.meta.put("chosenMimeType", mimeTypes.split(",")[0]); } } /** * @return the clazz */ public Class<?> getClazz() { return clazz; } /** * @return the defaultValue */ public Object getDefaultValue() { return defaultValue; } public Map<String, String> getMeta() { return meta; } }