package org.red5.server.net.remoting; /* * RED5 Open Source Flash Server - http://www.osflash.org/red5 * * Copyright (c) 2006-2009 by respective authors (see below). All rights reserved. * * This library is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation; either version 2.1 of the License, or (at your option) any later * version. * * This library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along * with this library; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpConnectionManager; import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.mina.common.ByteBuffer; import org.red5.io.amf.Input; import org.red5.io.amf.Output; import org.red5.io.object.Deserializer; import org.red5.io.object.RecordSet; import org.red5.io.object.Serializer; import org.red5.server.net.servlet.ServletUtils; import org.red5.server.pooling.ThreadPool; import org.red5.server.pooling.Worker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Client interface for remoting calls. * * @author The Red5 Project (red5@osflash.org) * @author Joachim Bauch (jojo@struktur.de) * */ public class RemotingClient { /** * Logger */ protected static Logger log = LoggerFactory.getLogger(RemotingClient.class); /** Default timeout to use. */ public static final int DEFAULT_TIMEOUT = 30000; /** Content MIME type for HTTP requests. */ private static final String CONTENT_TYPE = "application/x-amf"; /** Manages HTTP connections. */ private static HttpConnectionManager connectionMgr = new MultiThreadedHttpConnectionManager(); /** HTTP client for remoting calls. */ private HttpClient client; /** Url to connect to. */ private String url; /** Additional string to use while connecting. */ private String appendToUrl = ""; /** Headers to send to the server. */ protected Map<String, RemotingHeader> headers = new ConcurrentHashMap<String, RemotingHeader>(); /** Thread pool to use for asynchronous requests. */ protected static ThreadPool threadPool; /** * Dummy constructor used by the spring configuration. */ public RemotingClient() { // Do nothing. log.debug("RemotingClient created"); } /** * Create new remoting client for the given url. * * @param url URL to connect to */ public RemotingClient(String url) { this(url, DEFAULT_TIMEOUT); log.debug("RemotingClient created - url: {}", url); } /** * Create new remoting client for the given url and given timeout. * * @param url URL to connect to * @param timeout Timeout for one request in milliseconds */ public RemotingClient(String url, int timeout) { this(url, timeout, connectionMgr); log.debug("RemotingClient created - url: {} timeout: {}", url, timeout); } /** * Create new remoting client in the given connection manager. * * @param url URL to connect to * @param mgr The connection manager to use */ public RemotingClient(String url, HttpConnectionManager mgr) { this(url, DEFAULT_TIMEOUT, mgr); log.debug("RemotingClient created - url: {} http manager: {}", url, mgr); } /** * Create new remoting client with the given connection manager and timeout. * * @param url URL to connect to * @param timeout Timeout for one request in milliseconds * @param mgr The connection manager to use */ public RemotingClient(String url, int timeout, HttpConnectionManager mgr) { client = new HttpClient(mgr); client.getHttpConnectionManager().getParams().setConnectionTimeout( timeout); this.url = url; log.debug("RemotingClient created - url: {} timeout: {} http manager: {}", new Object[]{url, timeout, mgr}); } /** * Set the thread pool to use for asynchronous requests. * * @param threadPool The thread pool */ public void setThreadPool(ThreadPool threadPool) { RemotingClient.threadPool = threadPool; log.debug("RemotingClient thread pool: {}", threadPool); } /** * Encode the method call. * * @param method * Remote method being called * @param params * Method parameters * @return Byte buffer with data to perform remoting call */ private ByteBuffer encodeInvoke(String method, Object[] params) { log.debug("RemotingClient encodeInvoke - method: {} params: {}", method, params); ByteBuffer result = ByteBuffer.allocate(1024); result.setAutoExpand(true); // XXX: which is the correct version? result.putShort((short) 0); // Headers Collection<RemotingHeader> hdr = headers.values(); result.putShort((short) hdr.size()); for (RemotingHeader header : hdr) { Output.putString(result, header.name); result.put(header.required ? (byte) 0x01 : (byte) 0x00); ByteBuffer tmp = ByteBuffer.allocate(1024); tmp.setAutoExpand(true); Output tmpOut = new Output(tmp); Serializer tmpSer = new Serializer(); tmpSer.serialize(tmpOut, header.data); tmp.flip(); // Size of header data result.putInt(tmp.limit()); // Header data result.put(tmp); tmp.release(); tmp = null; } // One body result.putShort((short) 1); // Method name Output.putString(result, method); // Client callback for response Output.putString(result, ""); // Serialize parameters ByteBuffer tmp = ByteBuffer.allocate(1024); tmp.setAutoExpand(true); Output tmpOut = new Output(tmp); //if the params are null send the NULL AMF type //this should fix APPSERVER-296 if (params == null) { tmpOut.writeNull(); } else { tmpOut.writeArray(params, new Serializer()); } tmp.flip(); // Store size and parameters result.putInt(tmp.limit()); result.put(tmp); tmp.release(); tmp = null; result.flip(); return result; } /** * Process any headers sent in the response. * * @param in * Byte buffer with response data */ @SuppressWarnings("static-access") protected void processHeaders(ByteBuffer in) { log.debug("RemotingClient processHeaders - buffer limit: {}", (in != null ? in.limit() : 0)); int version = in.getUnsignedShort(); // skip log.debug("Version: {}", version); // the version by now, AMF3 is not yet implemented int count = in.getUnsignedShort(); log.debug("Count: {}", count); Deserializer deserializer = new Deserializer(); Input input = new Input(in); for (int i = 0; i < count; i++) { String name = input.getString(in); //String name = deserializer.deserialize(input, String.class); log.debug("Name: {}", name); boolean required = (in.get() == 0x01); log.debug("Required: {}", required); int len = in.getInt(); log.debug("Length: {}", len); Object value = deserializer.deserialize(input, Object.class); log.debug("Value: {}", value); // XXX: this is pretty much untested!!! if (RemotingHeader.APPEND_TO_GATEWAY_URL.equals(name)) { // Append string to gateway url appendToUrl = (String) value; } else if (RemotingHeader.REPLACE_GATEWAY_URL.equals(name)) { // Replace complete gateway url url = (String) value; // XXX: reset the <appendToUrl< here? } else if (RemotingHeader.PERSISTENT_HEADER.equals(name)) { // Send a new header with each following request if (value instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> valueMap = (Map<String, Object>) value; RemotingHeader header = new RemotingHeader( (String) valueMap.get("name"), (Boolean) valueMap.get("mustUnderstand"), valueMap.get("data")); headers.put(header.name, header); } else { log.error("Expected Map but received {}", value); } } else { log.warn("Unsupported remoting header \"{}\" received with value \"{}\"", name, value); } } } /** * Decode response received from remoting server. * * @param data * Result data to decode * @return Object deserialized from byte buffer data */ private Object decodeResult(ByteBuffer data) { log.debug("decodeResult - data limit: {}", (data != null ? data.limit() : 0)); processHeaders(data); int count = data.getUnsignedShort(); if (count != 1) { throw new RuntimeException("Expected exactly one result but got " + count); } Input input = new Input(data); String target = input.getString(); // expect "/onResult" log.debug("Target: {}", target); String nullString = input.getString(); // expect "null" log.debug("Null string: {}", nullString); // Read return value Deserializer deserializer = new Deserializer(); return deserializer.deserialize(input, Object.class); } /** * Send authentication data with each remoting request. * * @param userid User identifier * @param password Password */ public void setCredentials(String userid, String password) { Map<String, String> data = new HashMap<String, String>(); data.put("userid", userid); data.put("password", password); RemotingHeader header = new RemotingHeader(RemotingHeader.CREDENTIALS, true, data); headers.put(RemotingHeader.CREDENTIALS, header); } /** * Stop sending authentication data. */ public void resetCredentials() { removeHeader(RemotingHeader.CREDENTIALS); } /** * Send an additional header to the server. * * @param name Header name * @param required Header required? * @param value Header body */ public void addHeader(String name, boolean required, Object value) { RemotingHeader header = new RemotingHeader(name, required, value); headers.put(name, header); } /** * Stop sending a given header. * * @param name Header name */ public void removeHeader(String name) { headers.remove(name); } /** * Invoke a method synchronously on the remoting server. * * @param method Method name * @param params Parameters passed to method * @return the result of the method call */ public Object invokeMethod(String method, Object[] params) { log.debug("invokeMethod url: {}", (url + appendToUrl)); PostMethod post = new PostMethod(this.url + appendToUrl); ByteBuffer resultBuffer = null; ByteBuffer data = encodeInvoke(method, params); post.setRequestEntity(new InputStreamRequestEntity( data.asInputStream(), data.limit(), CONTENT_TYPE)); try { log.debug("Client: {}", client); int resultCode = client.executeMethod(post); log.debug("Result code: {}", resultCode); if (resultCode / 100 != 2) { throw new RuntimeException( "Didn't receive success from remoting server."); } resultBuffer = ByteBuffer.allocate((int) post .getResponseContentLength()); ServletUtils.copy(post.getResponseBodyAsStream(), resultBuffer .asOutputStream()); resultBuffer.flip(); Object result = decodeResult(resultBuffer); if (result instanceof RecordSet) { // Make sure we can retrieve paged results ((RecordSet) result).setRemotingClient(this); } return result; } catch (Exception ex) { log.error("Error while invoking remoting method.", ex); } finally { post.releaseConnection(); if (resultBuffer != null) { resultBuffer.release(); resultBuffer = null; } data.release(); data = null; } return null; } /** * Invoke a method asynchronously on the remoting server. * * @param method Method name * @param methodParams Parameters passed to method * @param callback Callback */ public void invokeMethod(String method, Object[] methodParams, IRemotingCallback callback) { if (threadPool == null) { throw new RuntimeException("No thread pool configured."); } try { Object[] params = new Object[] { this, method, methodParams, callback }; Class<?>[] paramTypes = new Class[] { RemotingClient.class, String.class, Object[].class, IRemotingCallback.class }; Worker worker = new Worker(); worker.setClassName("org.red5.server.net.remoting.RemotingClient$RemotingWorker"); worker.setMethodName("executeTask"); worker.setMethodParams(params); worker.setParamTypes(paramTypes); threadPool.execute(worker); } catch (Exception err) { log.warn("Exception invoking method: {}", method, err); } } /** * Worker class that is used for asynchronous remoting calls. */ public static class RemotingWorker { /** * Execute task. * * @param client Remoting client * @param method Method name * @param params Parameters to pass to method on call * @param callback Callback */ public void executeTask(RemotingClient client, String method, Object[] params, IRemotingCallback callback) { try { Object result = client.invokeMethod(method, params); callback.resultReceived(client, method, params, result); } catch (Exception err) { callback.errorReceived(client, method, params, err); } } } }