/*
* This file is part of Transdroid <http://www.transdroid.org>
*
* Transdroid 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, either version 3 of the License, or
* (at your option) any later version.
*
* Transdroid 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 Transdroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.transdroid.daemon.Vuze;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.base64.android.Base64;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.util.TlsSniSocketFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import android.util.Xml;
/**
* Implements an XML-RPC-like client that build and parses XML following
* Vuze's XML over HTTP plug-in (which unfortunately is incompatible with
* the default XML-RPC protocol).
*
* The documentation can be found at http://azureus.sourceforge.net/plugin_details.php?plugin=xml_http_if&docu=1#1
* and some stuff is at http://wiki.vuze.com/index.php/XML_over_HTTP
*
* A lot of it is copied from the org.xmlrpc.android library's XMLRPCClient
* as can be found at http://code.google.com/p/android-xmlrpc
*
* @author erickok
*
*/
public class VuzeXmlOverHttpClient {
private final static String TAG_REQUEST = "REQUEST";
private final static String TAG_OBJECT = "OBJECT";
private final static String TAG_OBJECT_ID = "_object_id";
private final static String TAG_METHOD = "METHOD";
private final static String TAG_PARAMS = "PARAMS";
private final static String TAG_ENTRY = "ENTRY";
private final static String TAG_INDEX = "index";
private final static String TAG_CONNECTION_ID = "CONNECTION_ID";
private final static String TAG_REQUEST_ID = "REQUEST_ID";
private final static String TAG_RESPONSE = "RESPONSE";
private final static String TAG_ERROR = "ERROR";
private final static String TAG_TORRENT = "torrent";
private final static String TAG_STATS = "stats";
private final static String TAG_ANNOUNCE = "announce_result";
private final static String TAG_SCRAPE = "scrape_result";
private final static String TAG_CACHED_PROPERTY_NAMES = "cached_property_names";
private DefaultHttpClient client;
private HttpPost postMethod;
private Random random;
private String username;
private String password;
/**
* XMLRPCClient constructor. Creates new instance based on server URI
* @param settings The server connection settings
* @param uri The URI of the XML RPC to connect to
* @throws DaemonException Thrown when settings are missing or conflicting
*/
public VuzeXmlOverHttpClient(DaemonSettings settings, URI uri) throws DaemonException {
postMethod = new HttpPost(uri);
postMethod.addHeader("Content-Type", "text/xml");
// WARNING
// I had to disable "Expect: 100-Continue" header since I had
// two second delay between sending http POST request and POST body
HttpParams httpParams = postMethod.getParams();
HttpProtocolParams.setUseExpectContinue(httpParams, false);
HttpConnectionParams.setConnectionTimeout(httpParams, settings.getTimeoutInMilliseconds());
HttpConnectionParams.setSoTimeout(httpParams, settings.getTimeoutInMilliseconds());
SchemeRegistry registry = new SchemeRegistry();
SocketFactory httpsSocketFactory;
if (settings.getSslTrustKey() != null) {
httpsSocketFactory = new TlsSniSocketFactory(settings.getSslTrustKey());
} else if (settings.getSslTrustAll()) {
httpsSocketFactory = new TlsSniSocketFactory(true);
} else {
httpsSocketFactory = new TlsSniSocketFactory();
}
registry.register(new Scheme("http", new PlainSocketFactory(), 80));
registry.register(new Scheme("https", httpsSocketFactory, 443));
client = new DefaultHttpClient(new ThreadSafeClientConnManager(httpParams, registry), httpParams);
if (settings.shouldUseAuthentication()) {
if (settings.getUsername() == null || settings.getPassword() == null) {
throw new DaemonException(DaemonException.ExceptionType.AuthenticationFailure, "No username or password set, while authentication was enabled.");
} else {
username = settings.getUsername();
password = settings.getPassword();
client.getCredentialsProvider().setCredentials(
new AuthScope(postMethod.getURI().getHost(), postMethod.getURI().getPort(), AuthScope.ANY_REALM),
new UsernamePasswordCredentials(username, password));
}
}
random = new Random();
random.nextInt();
}
/**
* Convenience constructor. Creates new instance based on server String address
* @param settings The server connection settings
* @param url The URL of the XML RPC to connect to
* @throws DaemonException Thrown when settings are missing or conflicting
*/
public VuzeXmlOverHttpClient(DaemonSettings settings, String url) throws DaemonException {
this(settings, URI.create(url));
}
protected Map<String, Object> callXMLRPC(Long object, String method, Object[] params, Long connectionID, boolean paramsAreVuzeObjects) throws DaemonException {
try {
// prepare POST body
XmlSerializer serializer = Xml.newSerializer();
StringWriter bodyWriter = new StringWriter();
serializer.setOutput(bodyWriter);
serializer.startDocument(null, null);
serializer.startTag(null, TAG_REQUEST);
// set object
if (object != null) {
serializer.startTag(null, TAG_OBJECT).startTag(null, TAG_OBJECT_ID)
.text(object.toString()).endTag(null, TAG_OBJECT_ID).endTag(null, TAG_OBJECT);
}
// set method
serializer.startTag(null, TAG_METHOD).text(method).endTag(null, TAG_METHOD);
if (params != null && params.length != 0) {
// set method params
serializer.startTag(null, TAG_PARAMS);
Integer entryIndex = 0;
for (Object param : params) {
serializer.startTag(null, TAG_ENTRY).attribute(null, TAG_INDEX, entryIndex.toString());
if (paramsAreVuzeObjects) {
serializer.startTag(null, TAG_OBJECT).startTag(null, TAG_OBJECT_ID);
}
serializer.text(serialize(param));
if (paramsAreVuzeObjects) {
serializer.endTag(null, TAG_OBJECT_ID).endTag(null, TAG_OBJECT);
}
serializer.endTag(null, TAG_ENTRY);
entryIndex++;
}
serializer.endTag(null, TAG_PARAMS);
}
// set connection id
if (connectionID != null) {
serializer.startTag(null, TAG_CONNECTION_ID).text(connectionID.toString()).endTag(null, TAG_CONNECTION_ID);
}
// set request id, which for this purpose is always a random number
Integer randomRequestID = new Integer(random.nextInt());
serializer.startTag(null, TAG_REQUEST_ID).text(randomRequestID.toString()).endTag(null, TAG_REQUEST_ID);
serializer.endTag(null, TAG_REQUEST);
serializer.endDocument();
// set POST body
HttpEntity entity = new StringEntity(bodyWriter.toString());
postMethod.setEntity(entity);
// Force preemptive authentication
// This makes sure there is an 'Authentication: ' header being send before trying and failing and retrying
// by the basic authentication mechanism of DefaultHttpClient
postMethod.addHeader("Authorization", "Basic " + Base64.encodeBytes((username + ":" + password).getBytes()));
// execute HTTP POST request
HttpResponse response = client.execute(postMethod);
// check status code
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
throw new DaemonException(ExceptionType.AuthenticationFailure, "HTTP " + HttpStatus.SC_UNAUTHORIZED + " response (so no user or password or incorrect ones)");
} else if (statusCode != HttpStatus.SC_OK) {
throw new DaemonException(ExceptionType.ConnectionError, "HTTP status code: " + statusCode + " != " + HttpStatus.SC_OK);
}
// parse response stuff
//
// setup pull parser
XmlPullParser pullParser = XmlPullParserFactory.newInstance().newPullParser();
entity = response.getEntity();
//String temp = HttpHelper.ConvertStreamToString(entity.getContent());
//Reader reader = new StringReader(temp);
Reader reader = new InputStreamReader(entity.getContent());
pullParser.setInput(reader);
// lets start pulling...
pullParser.nextTag();
pullParser.require(XmlPullParser.START_TAG, null, TAG_RESPONSE);
// build list of returned values
int next = pullParser.nextTag(); // skip to first start tag in list
String name = pullParser.getName(); // get name of the first start tag
// Empty response?
if (next == XmlPullParser.END_TAG && name.equals(TAG_RESPONSE)) {
return null;
} else if (name.equals(TAG_ERROR)) {
// Error
String errorText = pullParser.nextText(); // the value of the ERROR
entity.consumeContent();
throw new DaemonException(ExceptionType.ConnectionError, errorText);
} else {
// Consume a list of ENTRYs?
if (name.equals(TAG_ENTRY)) {
Map<String, Object> entries = new HashMap<String, Object>();
for (int i = 0; name.equals(TAG_ENTRY); i++) {
entries.put(TAG_ENTRY + i, consumeEntry(pullParser));
name = pullParser.getName();
}
entity.consumeContent();
return entries;
} else {
// Only a single object was returned, not an entry listing
return consumeObject(pullParser);
}
}
} catch (IOException e) {
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
} catch (XmlPullParserException e) {
throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
}
}
private Map<String, Object> consumeEntry(XmlPullParser pullParser) throws XmlPullParserException, IOException {
int next = pullParser.nextTag();
String name = pullParser.getName();
// Consume the ENTRY objects
Map<String, Object> returnValues = new HashMap<String, Object>();
while (next == XmlPullParser.START_TAG) {
if (name.equals(TAG_TORRENT) || name.equals(TAG_ANNOUNCE) || name.equals(TAG_SCRAPE) || name.equals(TAG_STATS)) {
// One of the objects contained inside an entry
pullParser.nextTag();
returnValues.put(name, consumeObject(pullParser));
} else {
// An object text inside this entry (such as _connectionid)
returnValues.put(name, deserialize(pullParser.nextText()));
}
next = pullParser.nextTag(); // skip to next start tag
name = pullParser.getName(); // get name of the new start tag
}
// Consume ENTRY ending
pullParser.nextTag();
return returnValues;
}
private Map<String, Object> consumeObject(XmlPullParser pullParser) throws XmlPullParserException, IOException {
int next = XmlPullParser.START_TAG;
String name = pullParser.getName();
// Consume bottom-level (contains no objects of its own) object
Map<String, Object> returnValues = new HashMap<String, Object>();
while (next == XmlPullParser.START_TAG && !(name.equals(TAG_CACHED_PROPERTY_NAMES))) {
if (name.equals(TAG_TORRENT) || name.equals(TAG_ANNOUNCE) || name.equals(TAG_SCRAPE) || name.equals(TAG_STATS)) {
// One of the objects contained inside an object
pullParser.nextTag();
returnValues.put(name, consumeObject(pullParser));
} else {
// An object text inside this entry (such as _connectionid)
returnValues.put(name, deserialize(pullParser.nextText()));
}
next = pullParser.nextTag(); // skip to next start tag
name = pullParser.getName(); // get name of the new start tag
}
return returnValues;
}
private String serialize(Object value) {
return value.toString();
}
static Object deserialize(String rawText) {
// Null?
if (rawText == null || rawText.equals("null")) {
return null;
}
/* For now cast all integers as Long; this prevents casting problems later on when
* we know it's a long but the value was small so it is casted to an Integer here
// Integer?
try {
Integer integernum = Integer.parseInt(rawText);
return integernum;
} catch (NumberFormatException e) {
// Just continue trying the next type
}*/
// Long?
try {
Long longnum = Long.parseLong(rawText);
return longnum;
} catch (NumberFormatException e) {
// Just continue trying the next type
}
// Double?
try {
Double doublenum = Double.parseDouble(rawText);
return doublenum;
} catch (NumberFormatException e) {
// Just continue trying the next type
}
// String otherwise
return rawText;
}
}