package de.timroes.axmlrpc; import de.timroes.axmlrpc.serializer.SerializerHandler; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.*; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import javax.net.ssl.*; /** * An XMLRPCClient is a client used to make XML-RPC (Extensible Markup Language * Remote Procedure Calls). * The specification of XMLRPC can be found at http://www.xmlrpc.com/spec. * You can use flags to extend the functionality of the client to some extras. * Further information on the flags can be found in the documentation of these. * For a documentation on how to use this class see also the README file delivered * with the source of this library. * * @author Tim Roes */ public class XMLRPCClient { private static final String DEFAULT_USER_AGENT = "aXMLRPC"; /** * Constants from the http protocol. */ static final String USER_AGENT = "User-Agent"; static final String CONTENT_TYPE = "Content-Type"; static final String TYPE_XML = "text/xml; charset=utf-8"; static final String HOST = "Host"; static final String CONTENT_LENGTH = "Content-Length"; static final String HTTP_POST = "POST"; /** * XML elements to be used. */ static final String METHOD_RESPONSE = "methodResponse"; static final String PARAMS = "params"; static final String PARAM = "param"; public static final String VALUE = "value"; static final String FAULT = "fault"; static final String METHOD_CALL = "methodCall"; static final String METHOD_NAME = "methodName"; static final String STRUCT_MEMBER = "member"; /** * No flags should be set. */ public static final int FLAGS_NONE = 0x0; /** * The client should parse responses strict to specification. * It will check if the given content-type is right. * The method name in a call must only contain of A-Z, a-z, 0-9, _, ., :, / * Normally this is not needed. */ public static final int FLAGS_STRICT = 0x01; /** * The client will be able to handle 8 byte integer values (longs). * The xml type tag <i8> will be used. This is not in the specification * but some libraries and servers support this behaviour. * If this isn't enabled you cannot recieve 8 byte integers and if you try to * send a long the value must be within the 4byte integer range. */ public static final int FLAGS_8BYTE_INT = 0x02; /** * With this flag, the client will be able to handle cookies, meaning saving cookies * from the server and sending it with every other request again. This is needed * for some XML-RPC interfaces that support login. */ public static final int FLAGS_ENABLE_COOKIES = 0x04; /** * The client will be able to send null values. A null value will be send * as <nil/>. This extension is described under: http://ontosys.com/xml-rpc/extensions.php */ public static final int FLAGS_NIL = 0x08; /** * With this flag enabled, the XML-RPC client will ignore the HTTP status * code of the response from the server. According to specification the * status code must be 200. This flag is only needed for the use with * not standard compliant servers. */ public static final int FLAGS_IGNORE_STATUSCODE = 0x10; /** * With this flag enabled, the client will forward the request, if * the 301 or 302 HTTP status code has been received. If this flag has not * been set, the client will throw an exception on these HTTP status codes. */ public static final int FLAGS_FORWARD = 0x20; /** * With this flag enabled, the client will ignore, if the URL doesn't match * the SSL Certificate. This should be used with caution. Normally the URL * should always match the URL in the SSL certificate, even with self signed * certificates. */ public static final int FLAGS_SSL_IGNORE_INVALID_HOST = 0x40; /** * With this flag enabled, the client will ignore all unverified SSL/TLS * certificates. This must be used, if you use self-signed certificates * or certificated from unknown (or untrusted) authorities. If this flag is * used, calls to {@link #installCustomTrustManager(javax.net.ssl.TrustManager)} * won't have any effect. */ public static final int FLAGS_SSL_IGNORE_INVALID_CERT = 0x80; /** * With this flag enabled, a value with a missing type tag, will be parsed * as a string element. This is just for incoming messages. Outgoing messages * will still be generated according to specification. */ public static final int FLAGS_DEFAULT_TYPE_STRING = 0x100; /** * With this flag enabled, the {@link XMLRPCClient} ignores all namespaces * used within the response from the server. */ public static final int FLAGS_IGNORE_NAMESPACES = 0x200; /** * With this flag enabled, the {@link XMLRPCClient} will use the system http * proxy to connect to the XML-RPC server. */ public static final int FLAGS_USE_SYSTEM_PROXY = 0x400; /** * This prevents the decoding of incoming strings, meaning & and < * won't be decoded to the & sign and the "less then" sign. See * {@link #FLAGS_NO_STRING_ENCODE} for the counterpart. */ public static final int FLAGS_NO_STRING_DECODE = 0x800; /** * By default outgoing string values will be encoded according to specification. * Meaning the & sign will be encoded to & and the "less then" sign to <. * If you set this flag, the encoding won't be done for outgoing string values. * See {@link #FLAGS_NO_STRING_ENCODE} for the counterpart. */ public static final int FLAGS_NO_STRING_ENCODE = 0x1000; /** * This flag disables all SSL warnings. It is an alternative to use * FLAGS_SSL_IGNORE_INVALID_CERT | FLAGS_SSL_IGNORE_INVALID_HOST. There * is no functional difference. */ public static final int FLAGS_SSL_IGNORE_ERRORS = FLAGS_SSL_IGNORE_INVALID_CERT | FLAGS_SSL_IGNORE_INVALID_HOST; /** * This flag should be used if the server is an apache ws xmlrpc server. * This will set some flags, so that the not standard conform behavior * of the server will be ignored. * This will enable the following flags: FLAGS_IGNORE_NAMESPACES, FLAGS_NIL, * FLAGS_DEFAULT_TYPE_STRING */ public static final int FLAGS_APACHE_WS = FLAGS_IGNORE_NAMESPACES | FLAGS_NIL | FLAGS_DEFAULT_TYPE_STRING; private final int flags; private URL url; private Map<String,String> httpParameters = new ConcurrentHashMap<String, String>(); private Map<Long,Caller> backgroundCalls = new ConcurrentHashMap<Long, Caller>(); private ResponseParser responseParser; private CookieManager cookieManager; private AuthenticationManager authManager; private TrustManager[] trustManagers; private KeyManager[] keyManagers; private Proxy proxy; private int timeout; /** * Create a new XMLRPC client for the given URL. * * @param url The URL to send the requests to. * @param userAgent A user agent string to use in the HTTP requests. * @param flags A combination of flags to be set. */ public XMLRPCClient(URL url, String userAgent, int flags) { SerializerHandler.initialize(flags); this.url = url; this.flags = flags; // Create a parser for the http responses. responseParser = new ResponseParser(); cookieManager = new CookieManager(flags); authManager = new AuthenticationManager(); httpParameters.put(CONTENT_TYPE, TYPE_XML); httpParameters.put(USER_AGENT, userAgent); // If invalid ssl certs are ignored, instantiate an all trusting TrustManager if(isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { trustManagers = new TrustManager[] { new X509TrustManager() { public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException { } public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return null; } } }; } if(isFlagSet(FLAGS_USE_SYSTEM_PROXY)) { // Read system proxy settings and generate a proxy from that Properties prop = System.getProperties(); String proxyHost = prop.getProperty("http.proxyHost"); int proxyPort = Integer.parseInt(prop.getProperty("http.proxyPort", "0")); if(proxyPort > 0 && proxyHost.length() > 0 && !proxyHost.equals("null")) { proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); } } } /** * Create a new XMLRPC client for the given URL. * The default user agent string will be used. * * @param url The URL to send the requests to. * @param flags A combination of flags to be set. */ public XMLRPCClient(URL url, int flags) { this(url, DEFAULT_USER_AGENT, flags); } /** * Create a new XMLRPC client for the given url. * No flags will be set. * * @param url The url to send the requests to. * @param userAgent A user agent string to use in the http request. */ public XMLRPCClient(URL url, String userAgent) { this(url, userAgent, FLAGS_NONE); } /** * Create a new XMLRPC client for the given url. * No flags will be used. * The default user agent string will be used. * * @param url The url to send the requests to. */ public XMLRPCClient(URL url) { this(url, DEFAULT_USER_AGENT, FLAGS_NONE); } /** * Returns the URL this XMLRPCClient is connected to. If that URL permanently forwards * to another URL, this method will return the forwarded URL, as soon as * the first call has been made. * * @return Returns the URL for this XMLRPCClient. */ public URL getURL() { return url; } /** * Sets the time in seconds after which a call should timeout. * If {@code timeout} will be zero or less the connection will never timeout. * In case the connection times out and {@link XMLRPCTimeoutException} will * be thrown for calls made by {@link #call(java.lang.String, java.lang.Object[])}. * For calls made by {@link #callAsync(de.timroes.axmlrpc.XMLRPCCallback, java.lang.String, java.lang.Object[])} * the {@link XMLRPCCallback#onError(long, de.timroes.axmlrpc.XMLRPCException)} method * of the callback will be called. By default connections won't timeout. * * @param timeout The timeout for connections in seconds. */ public void setTimeout(int timeout) { this.timeout = timeout; } /** * Sets the user agent string. * If this method is never called the default * user agent 'aXMLRPC' will be used. * * @param userAgent The new user agent string. */ public void setUserAgentString(String userAgent) { httpParameters.put(USER_AGENT, userAgent); } /** * Sets a proxy to use for this client. If you want to use the system proxy, * use {@link #FLAGS_adbUSE_SYSTEM_PROXY} instead. If combined with * {@code FLAGS_USE_SYSTEM_PROXY}, this proxy will be used instead of the * system proxy. * * @param proxy A proxy to use for the connection. */ public void setProxy(Proxy proxy) { this.proxy = proxy; } /** * Set a HTTP header field to a custom value. * You cannot modify the Host or Content-Type field that way. * If the field already exists, the old value is overwritten. * * @param headerName The name of the header field. * @param headerValue The new value of the header field. */ public void setCustomHttpHeader(String headerName, String headerValue) { if(CONTENT_TYPE.equals(headerName) || HOST.equals(headerName) || CONTENT_LENGTH.equals(headerName)) { throw new XMLRPCRuntimeException("You cannot modify the Host, Content-Type or Content-Length header."); } httpParameters.put(headerName, headerValue); } /** * Set the username and password that should be used to perform basic * http authentication. * * @param user Username * @param pass Password */ public void setLoginData(String user, String pass) { authManager.setAuthData(user, pass); } /** * Clear the username and password. No basic HTTP authentication will be used * in the next calls. */ public void clearLoginData() { authManager.clearAuthData(); } /** * Returns a {@link Map} of all cookies. It contains each cookie key as a map * key and its value as a map value. Cookies will only be used if {@link #FLAGS_ENABLE_COOKIES} * has been set for the client. This map will also be available (and empty) * when this flag hasn't been said, but has no effect on the HTTP connection. * * @return A {@code Map} of all cookies. */ public Map<String,String> getCookies() { return cookieManager.getCookies(); } /** * Delete all cookies currently used by the client. * This method has only an effect, as long as the FLAGS_ENABLE_COOKIES has * been set on this client. */ public void clearCookies() { cookieManager.clearCookies(); } /** * Installs a custom {@link TrustManager} to handle SSL/TLS certificate verification. * This will replace any previously installed {@code TrustManager}s. * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. * * @param trustManager {@link TrustManager} to install. * * @see #installCustomTrustManagers(javax.net.ssl.TrustManager[]) */ public void installCustomTrustManager(TrustManager trustManager) { if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { trustManagers = new TrustManager[] { trustManager }; } } /** * Installs custom {@link TrustManager TrustManagers} to handle SSL/TLS certificate * verification. This will replace any previously installed {@code TrustManagers}s. * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. * * @param trustManagers {@link TrustManager TrustManagers} to install. * * @see #installCustomTrustManager(javax.net.ssl.TrustManager) */ public void installCustomTrustManagers(TrustManager[] trustManagers) { if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { this.trustManagers = trustManagers.clone(); } } /** * Installs a custom {@link KeyManager} to handle SSL/TLS certificate verification. * This will replace any previously installed {@code KeyManager}s. * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. * * @param keyManager {@link KeyManager} to install. * * @see #installCustomKeyManagers(javax.net.ssl.KeyManager[]) */ public void installCustomKeyManager(KeyManager keyManager) { if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { keyManagers = new KeyManager[] { keyManager }; } } /** * Installs custom {@link KeyManager KeyManagers} to handle SSL/TLS certificate * verification. This will replace any previously installed {@code KeyManagers}s. * If {@link #FLAGS_SSL_IGNORE_INVALID_CERT} is set, this won't do anything. * * @param keyManagers {@link KeyManager KeyManagers} to install. * * @see #installCustomKeyManager(javax.net.ssl.KeyManager) */ public void installCustomKeyManagers(KeyManager[] keyManagers) { if(!isFlagSet(FLAGS_SSL_IGNORE_INVALID_CERT)) { this.keyManagers = keyManagers.clone(); } } /** * Call a remote procedure on the server. The method must be described by * a method name. If the method requires parameters, this must be set. * The type of the return object depends on the server. You should consult * the server documentation and then cast the return value according to that. * This method will block until the server returned a result (or an error occurred). * Read the README file delivered with the source code of this library for more * information. * * @param method A method name to call. * @param params An array of parameters for the method. * @return The result of the server. * @throws XMLRPCException Will be thrown if an error occurred during the call. */ public Object call(String method, Object... params) throws XMLRPCException { return new Caller().call(method, params); } /** * Asynchronously call a remote procedure on the server. The method must be * described by a method name. If the method requires parameters, this must * be set. When the server returns a response the onResponse method is called * on the listener. If the server returns an error the onServerError method * is called on the listener. The onError method is called whenever something * fails. This method returns immediately and returns an identifier for the * request. All listener methods get this id as a parameter to distinguish between * multiple requests. * * @param listener A listener, which will be notified about the server response or errors. * @param methodName A method name to call on the server. * @param params An array of parameters for the method. * @return The id of the current request. */ public long callAsync(XMLRPCCallback listener, String methodName, Object... params) { long id = System.currentTimeMillis(); new Caller(listener, id, methodName, params).start(); return id; } /** * Cancel a specific asynchronous call. * * @param id The id of the call as returned by the callAsync method. */ public void cancel(long id) { // Lookup the background call for the given id. Caller cancel = backgroundCalls.get(id); if(cancel == null) { return; } // Cancel the thread cancel.cancel(); try { // Wait for the thread cancel.join(); } catch (InterruptedException ex) { // Ignore this } } /** * Create a call object from a given method string and parameters. * * @param method The method that should be called. * @param params An array of parameters or null if no parameters needed. * @return A call object. */ private Call createCall(String method, Object[] params) { if(isFlagSet(FLAGS_STRICT) && !method.matches("^[A-Za-z0-9\\._:/]*$")) { throw new XMLRPCRuntimeException("Method name must only contain A-Z a-z . : _ / "); } return new Call(method, params); } /** * Checks whether a specific flag has been set. * * @param flag The flag to check for. * @return Whether the flag has been set. */ private boolean isFlagSet(int flag) { return (this.flags & flag) != 0; } /** * The Caller class is used to make asynchronous calls to the server. * For synchronous calls the Thread function of this class isn't used. */ private class Caller extends Thread { private XMLRPCCallback listener; private long threadId; private String methodName; private Object[] params; private volatile boolean canceled; private HttpURLConnection http; /** * Create a new Caller for asynchronous use. * * @param listener The listener to notice about the response or an error. * @param threadId An id that will be send to the listener. * @param methodName The method name to call. * @param params The parameters of the call or null. */ public Caller(XMLRPCCallback listener, long threadId, String methodName, Object[] params) { this.listener = listener; this.threadId = threadId; this.methodName = methodName; this.params = params; } /** * Create a new Caller for synchronous use. * If the caller has been created with this constructor you cannot use the * start method to start it as a thread. But you can call the call method * on it for synchronous use. */ public Caller() { } /** * The run method is invoked when the thread gets started. * This will only work, if the Caller has been created with parameters. * It execute the call method and notify the listener about the result. */ @Override public void run() { if(listener == null) return; try { backgroundCalls.put(threadId, this); Object o = this.call(methodName, params); listener.onResponse(threadId, o); } catch(CancelException ex) { // Don't notify the listener, if the call has been canceled. } catch(XMLRPCServerException ex) { listener.onServerError(threadId, ex); } catch (XMLRPCException ex) { listener.onError(threadId, ex); } finally { backgroundCalls.remove(threadId); } } /** * Cancel this call. This will abort the network communication. */ public void cancel() { // Set the flag, that this thread has been canceled canceled = true; // Disconnect the connection to the server http.disconnect(); } /** * Call a remote procedure on the server. The method must be described by * a method name. If the method requires parameters, this must be set. * The type of the return object depends on the server. You should consult * the server documentation and then cast the return value according to that. * This method will block until the server returned a result (or an error occurred). * Read the README file delivered with the source code of this library for more * information. * * @param method A method name to call. * @param params An array of parameters for the method. * @return The result of the server. * @throws XMLRPCException Will be thrown if an error occurred during the call. */ public Object call(String methodName, Object[] params) throws XMLRPCException { try { Call c = createCall(methodName, params); // If proxy is available, use it URLConnection conn; if(proxy != null) conn = url.openConnection(proxy); else conn = url.openConnection(); http = verifyConnection(conn); http.setInstanceFollowRedirects(false); http.setRequestMethod(HTTP_POST); http.setDoOutput(true); http.setDoInput(true); // Set timeout if(timeout > 0) { http.setConnectTimeout(timeout * 1000); http.setReadTimeout(timeout * 1000); } // Set the request parameters for(Map.Entry<String,String> param : httpParameters.entrySet()) { http.setRequestProperty(param.getKey(), param.getValue()); } authManager.setAuthentication(http); cookieManager.setCookies(http); OutputStreamWriter stream = new OutputStreamWriter(http.getOutputStream()); stream.write(c.getXML()); stream.flush(); stream.close(); // Try to get the status code from the connection int statusCode; try { statusCode = http.getResponseCode(); } catch(IOException ex) { // Due to a bug on android, the getResponseCode()-method will // fail the first time, with a IOException, when 401 or 403 has been returned. // The second time it should success. If it fail the second time again // the normal exceptipon handling can take care of this, since // it is a real error. statusCode = http.getResponseCode(); } InputStream istream; // If status code was 401 or 403 throw exception or if appropriate // flag is set, ignore error code. if(statusCode == HttpURLConnection.HTTP_FORBIDDEN || statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) { if(isFlagSet(FLAGS_IGNORE_STATUSCODE)) { // getInputStream will fail if server returned above // error code, use getErrorStream instead istream = http.getErrorStream(); } else { throw new XMLRPCException("Invalid status code '" + statusCode + "' returned from server."); } } else { istream = http.getInputStream(); } // If status code is 301 Moved Permanently or 302 Found ... if(statusCode == HttpURLConnection.HTTP_MOVED_PERM || statusCode == HttpURLConnection.HTTP_MOVED_TEMP) { // ... do either a foward if(isFlagSet(FLAGS_FORWARD)) { boolean temporaryForward = (statusCode == HttpURLConnection.HTTP_MOVED_TEMP); // Get new location from header field. String newLocation = http.getHeaderField("Location"); // Try getting header in lower case, if no header has been found if(newLocation == null || newLocation.length() <= 0) newLocation = http.getHeaderField("location"); // Set new location, disconnect current connection and request to new location. URL oldURL = url; url = new URL(newLocation); http.disconnect(); Object forwardedResult = call(methodName, params); // In case of temporary forward, restore original URL again for next call. if(temporaryForward) { url = oldURL; } return forwardedResult; } else { // ... or throw an exception throw new XMLRPCException("The server responded with a http 301 or 302 status " + "code, but forwarding has not been enabled (FLAGS_FORWARD)."); } } if(!isFlagSet(FLAGS_IGNORE_STATUSCODE) && statusCode != HttpURLConnection.HTTP_OK) { throw new XMLRPCException("The status code of the http response must be 200."); } // Check for strict parameters if(isFlagSet(FLAGS_STRICT)) { if(!http.getContentType().startsWith(TYPE_XML)) { throw new XMLRPCException("The Content-Type of the response must be text/xml."); } } cookieManager.readCookies(http); return responseParser.parse(istream); } catch(SocketTimeoutException ex) { throw new XMLRPCTimeoutException("The XMLRPC call timed out."); } catch (IOException ex) { // If the thread has been canceled this exception will be thrown. // So only throw an exception if the thread hasnt been canceled // or if the thred has not been started in background. if(!canceled || threadId <= 0) { throw new XMLRPCException(ex); } else { throw new CancelException(); } } } /** * Verifies the given URLConnection to be a valid HTTP or HTTPS connection. * If the SSL ignoring flags are set, the method will ignore SSL warnings. * * @param conn The URLConnection to validate. * @return The verified HttpURLConnection. * @throws XMLRPCException Will be thrown if an error occurred. */ private HttpURLConnection verifyConnection(URLConnection conn) throws XMLRPCException { if(!(conn instanceof HttpURLConnection)) { throw new IllegalArgumentException("The URL is not valid for a http connection."); } // Validate the connection if its an SSL connection if(conn instanceof HttpsURLConnection) { HttpsURLConnection h = (HttpsURLConnection)conn; // Don't check, that URL matches the certificate. if(isFlagSet(FLAGS_SSL_IGNORE_INVALID_HOST)) { h.setHostnameVerifier(new HostnameVerifier() { public boolean verify(String host, SSLSession ssl) { return true; } }); } // Associate the TrustManager with TLS and SSL connections, if present. if(trustManagers != null) { try { String[] sslContexts = new String[]{ "TLS", "SSL" }; for(String ctx : sslContexts) { SSLContext sc = SSLContext.getInstance(ctx); sc.init(keyManagers, trustManagers, new SecureRandom()); h.setSSLSocketFactory(sc.getSocketFactory()); } } catch(Exception ex) { throw new XMLRPCException(ex); } } return h; } return (HttpURLConnection)conn; } } private class CancelException extends RuntimeException { } }