package org.sugr.gearshift.service;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Base64;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.sugr.gearshift.G;
import org.sugr.gearshift.core.Torrent;
import org.sugr.gearshift.core.TransmissionProfile;
import org.sugr.gearshift.core.TransmissionSession;
import org.sugr.gearshift.datasource.DataSource;
import org.sugr.gearshift.datasource.TorrentStatus;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class TransmissionSessionManager {
public class ManagerException extends Exception {
private static final long serialVersionUID = 6477491498169428449L;
private int code;
public ManagerException(String message, int code) {
super(message == null ? "" : message);
this.code = code;
}
public int getCode() {
return code;
}
}
public final static String PREF_LAST_SESSION_ID = "last_session_id";
private TransmissionProfile profile;
private ConnectivityManager connManager;
private String sessionId;
private int invalidSessionRetries = 0;
private SharedPreferences defaultPrefs;
private DataSource dataSource;
private ConnectionProvider connProvider;
public TransmissionSessionManager(ConnectivityManager connManager, SharedPreferences prefs,
TransmissionProfile profile, DataSource dataSource,
ConnectionProvider connProvider) {
this.profile = profile;
this.dataSource = dataSource;
this.connManager = connManager;
this.defaultPrefs = prefs;
this.connProvider = connProvider;
sessionId = defaultPrefs.getString(PREF_LAST_SESSION_ID, null);
}
public boolean hasConnectivity() {
NetworkInfo info = connManager.getActiveNetworkInfo();
return (info != null);
}
public void setProfile(TransmissionProfile profile) {
if (this.profile == null && profile != null
|| !this.profile.getId().equals(profile.getId())) {
this.profile = profile;
sessionId = null;
}
}
public void updateSession() throws ManagerException {
ObjectNode request = createRequest("session-get");
SessionGetResponse response = new SessionGetResponse();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
}
public TorrentStatus getActiveTorrents(String[] fields) throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest("torrent-get", true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
arguments.put("ids", "recently-active");
arguments.put("fields", mapper.valueToTree(fields));
TorrentGetResponse response = new TorrentGetResponse();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
return response.getTorrentStatus();
}
public TorrentStatus getTorrents(String[] fields, String[] hashStrings, boolean removeObsolete)
throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest("torrent-get", true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
arguments.put("fields", mapper.valueToTree(fields));
if (hashStrings != null && hashStrings.length > 0) {
arguments.put("ids", mapper.valueToTree(hashStrings));
}
TorrentGetResponse response = new TorrentGetResponse();
response.setRemoveObsolete(removeObsolete);
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
return response.getTorrentStatus();
}
public void setSession(TransmissionSession session, String... keys) throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest("session-set");
ObjectNode arguments = mapper.valueToTree(session);
arguments.retain(keys);
request.put("arguments", arguments);
Response response = new Response();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
}
public String addTorrent(String uri, String meta, String location, boolean paused)
throws ManagerException {
ObjectNode request = createRequest("torrent-add", true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
if (uri == null) {
arguments.put(Torrent.AddFields.META, meta);
} else {
arguments.put(Torrent.AddFields.URI, uri);
}
if (location != null) {
arguments.put(Torrent.AddFields.LOCATION, location);
}
arguments.put(Torrent.AddFields.PAUSED, paused);
AddTorrentResponse response = new AddTorrentResponse();
response.setLocation(location);
requestData(request, response);
if (response.isDuplicate()) {
throw new ManagerException("duplicate torrent", -2);
} else if ("success".equals(response.getResult())) {
return response.getAddedHash();
} else {
throw new ManagerException(response.getResult(), -2);
}
}
public void removeTorrent(String[] hashStrings, boolean delete) throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest("torrent-remove", true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
arguments.put("ids", mapper.valueToTree(hashStrings));
arguments.put("delete-local-data", delete);
Response response = new Response();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
dataSource.removeTorrents(hashStrings);
}
public void setTorrentAction(String[] hashStrings, String action) throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest(action, true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
arguments.put("ids", mapper.valueToTree(hashStrings));
Response response = new Response();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
}
public void setTorrentQueueAction(String[] hashStrings, String action) throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest(action, true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
arguments.put("ids", mapper.valueToTree(hashStrings));
Response response = new Response();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
}
public void setTorrentLocation(String[] hashStrings, String location, boolean move) throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest("torrent-set-location", true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
arguments.put("ids", mapper.valueToTree(hashStrings));
arguments.put("location", location);
arguments.put("move", move);
Response response = new Response();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
}
@SuppressWarnings("unchecked")
public void setTorrentProperty(String[] hashStrings, String key, Object value) throws ManagerException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode request = createRequest("torrent-set", true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
arguments.put("ids", mapper.valueToTree(hashStrings));
if (key.equals(Torrent.SetterFields.TRACKER_REPLACE)) {
List<String> tuple = (List<String>) value;
ArrayNode list = JsonNodeFactory.instance.arrayNode();
for (int i = 0; i < tuple.size(); i += 2) {
list.add(Integer.parseInt(tuple.get(i))).add(tuple.get(i + 1));
}
arguments.put(key, list);
} else {
switch (key) {
case Torrent.SetterFields.DOWNLOAD_LIMITED:
case Torrent.SetterFields.SESSION_LIMITS:
case Torrent.SetterFields.UPLOAD_LIMITED:
arguments.put(key, (Boolean) value);
break;
case Torrent.SetterFields.TORRENT_PRIORITY:
case Torrent.SetterFields.QUEUE_POSITION:
case Torrent.SetterFields.PEER_LIMIT:
case Torrent.SetterFields.SEED_RATIO_MODE:
arguments.put(key, (Integer) value);
break;
case Torrent.SetterFields.FILES_WANTED:
case Torrent.SetterFields.FILES_UNWANTED:
if (value instanceof Integer) {
arguments.put(key, mapper.valueToTree(new int[]{(Integer) value}));
} else {
arguments.put(key, mapper.valueToTree(value));
}
break;
case Torrent.SetterFields.DOWNLOAD_LIMIT:
case Torrent.SetterFields.UPLOAD_LIMIT:
arguments.put(key, (Long) value);
break;
case Torrent.SetterFields.SEED_RATIO_LIMIT:
arguments.put(key, (Float) value);
break;
case Torrent.SetterFields.FILES_HIGH:
case Torrent.SetterFields.FILES_NORMAL:
case Torrent.SetterFields.FILES_LOW:
case Torrent.SetterFields.TRACKER_REMOVE:
arguments.put(key, mapper.valueToTree(value));
break;
case Torrent.SetterFields.TRACKER_ADD:
arguments.put(key, mapper.valueToTree(value));
break;
default:
throw new IllegalArgumentException("Invalid setter key");
}
}
Response response = new Response();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
switch (key) {
case Torrent.SetterFields.TRACKER_REMOVE:
List<Integer> idList = (List<Integer>) value;
int[] ids = new int[idList.size()];
Iterator<Integer> iterator = idList.iterator();
for (int i = 0; i < ids.length; ++i) {
ids[i] = iterator.next().intValue();
}
for (String hash : hashStrings) {
dataSource.removeTrackers(hash, ids);
}
break;
}
}
public boolean testPort() throws ManagerException {
ObjectNode request = createRequest("port-test");
PortTestResponse response = new PortTestResponse();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
return response.isPortOpen();
}
public long updateBlocklist() throws ManagerException {
ObjectNode request = createRequest("blocklist-update");
BlocklistUpdateResponse response = new BlocklistUpdateResponse();
requestData(request, response);
if (!"success".equals(response.getResult())) {
throw new ManagerException(response.getResult(), -2);
}
return response.getBlocklistSize();
}
public long getFreeSpace(String defaultPath) throws ManagerException {
ObjectNode request = createRequest("free-space", true);
ObjectNode arguments = (ObjectNode) request.path("arguments");
String path = defaultPrefs.getString(G.PREF_LIST_DIRECTORY, null);
if (path == null) {
path = defaultPath;
}
arguments.put("path", path);
FreeSpaceResponse response = new FreeSpaceResponse();
requestData(request, response);
if ("success".equals(response.getResult())) {
return response.getFreeSpace();
} else if ("method name not recognized".equals(response.getResult())) {
return -1;
} else {
G.logE("Transmission Daemon Error!",
new Exception(response.getResult()));
return -1;
}
}
private void requestData(ObjectNode data, Response response) throws ManagerException {
requestData(data, response, null);
}
private void requestData(ObjectNode data, Response response, String urlLocation) throws ManagerException {
if (!hasConnectivity()) {
throw new ManagerException("connectivity", -1);
}
OutputStream os = null;
InputStream is = null;
HttpURLConnection conn = null;
try {
if (urlLocation == null) {
conn = connProvider.open(profile);
} else {
Proxy proxy = connProvider.getProxy(profile);
conn = connProvider.open(urlLocation, proxy);
}
if (profile.isUseSSL()) {
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[] {
new X509TrustManager() {
@Override public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override public void checkClientTrusted(
java.security.cert.X509Certificate[] certs,
String authType) {}
@Override public void checkServerTrusted(
java.security.cert.X509Certificate[] certs,
String authType) {}
}
}, new java.security.SecureRandom());
((HttpsURLConnection) conn).setSSLSocketFactory(sc.getSocketFactory());
((HttpsURLConnection) conn).setHostnameVerifier(new HostnameVerifier() {
@Override public boolean verify(String hostname, SSLSession session) {
return true;
}
});
} catch (NoSuchAlgorithmException e) {
G.logE("Error creating an SSL context", e);
throw new ManagerException("ssl", -1);
} catch (KeyManagementException e) {
G.logE("Error initializing the SSL context", e);
throw new ManagerException("ssl", -1);
}
}
int timeout = profile.getTimeout() > 0
? profile.getTimeout() * 1000
: 10000;
conn.setReadTimeout(timeout);
conn.setConnectTimeout(timeout);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
conn.setUseCaches(false);
conn.setAllowUserInteraction(false);
conn.setRequestMethod("POST");
conn.setDoInput(true);
conn.setDoOutput(true);
if (sessionId != null) {
conn.setRequestProperty("X-Transmission-Session-Id", sessionId);
}
String user = profile.getUsername();
if (user != null && user.length() > 0) {
conn.setRequestProperty("Authorization",
"Basic " + Base64.encodeToString(
(user + ":" + profile.getPassword()).getBytes(), Base64.DEFAULT));
}
String json = data.toString();
os = conn.getOutputStream();
os.write(json.getBytes());
os.flush();
os.close();
G.logD("The request is " + json);
// Starts the query
conn.connect();
int code = conn.getResponseCode();
if (code == HttpURLConnection.HTTP_CONFLICT) {
sessionId = getSessionId(conn);
if (invalidSessionRetries < 3 && sessionId != null) {
++invalidSessionRetries;
requestData(data, response);
return;
} else {
invalidSessionRetries = 0;
}
} else {
invalidSessionRetries = 0;
}
switch(code) {
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_MULT_CHOICE:
case 307:
String location = conn.getHeaderField("Location");
if (location == null) {
throw new ManagerException(conn.getResponseMessage(), code);
} else {
requestData(data, response, location);
}
return;
case HttpURLConnection.HTTP_OK:
case HttpURLConnection.HTTP_CREATED:
if (conn.getHeaderField("Content-Type").startsWith("text/html")) {
throw new ManagerException("no-json", code);
}
is = conn.getInputStream();
// Convert the InputStream into a string
String encoding = conn.getContentEncoding();
if (encoding != null && encoding.equalsIgnoreCase("gzip")) {
is = new GZIPInputStream(is);
} else if (encoding != null && encoding.equalsIgnoreCase("deflate")) {
is = new InflaterInputStream(is);
}
buildResponse(is, response);
G.logD("Torrent response is '" + response.getResult() + "'");
break;
default:
throw new ManagerException(conn.getResponseMessage(), code);
}
} catch (java.net.SocketTimeoutException e) {
throw new ManagerException("timeout", -1);
} catch (JsonParseException | JsonMappingException e) {
G.logE("Error parsing JSON", e);
throw new ManagerException(e.getMessage(), -4);
} catch (IOException e) {
G.logE("Error reading stream", e);
throw new ManagerException(e.getMessage(), -1);
} catch (OutOfMemoryError e) {
e.printStackTrace();
throw new ManagerException("out of memory", -3);
} finally {
try {
if (os != null)
os.close();
if (is != null)
is.close();
} catch(IOException e) {
e.printStackTrace();
}
if (conn != null)
conn.disconnect();
}
}
private String getSessionId(HttpURLConnection conn) {
String id = conn.getHeaderField("X-Transmission-Session-Id");
if (id != null && !id.equals("") && !id.equals(defaultPrefs.getString(PREF_LAST_SESSION_ID, null))) {
Editor e = defaultPrefs.edit();
e.putString(PREF_LAST_SESSION_ID, id);
e.commit();
}
return id;
}
private ObjectNode createRequest(String method, boolean hasArguments) {
JsonNodeFactory factory = JsonNodeFactory.instance;
ObjectNode root = factory.objectNode();
root.put("method", method);
if (hasArguments) {
ObjectNode arguments = factory.objectNode();
root.put("arguments", arguments);
}
return root;
}
private ObjectNode createRequest(String method) {
return createRequest(method, false);
}
private void buildResponse(InputStream stream, Response response) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
JsonFactory factory = mapper.getFactory();
JsonParser parser = factory.createParser(stream);
if (parser.nextToken() != JsonToken.START_OBJECT) {
throw new IOException("The server data is expected to be an object");
}
while (parser.nextToken() != JsonToken.END_OBJECT) {
String name = parser.getCurrentName();
switch (name) {
case "result":
response.setResult(parser.nextTextValue());
break;
case "arguments":
if (response.getClass() == SessionGetResponse.class) {
parser.nextValue();
dataSource.updateSession(profile.getId(), parser);
} else if (response.getClass() == TorrentGetResponse.class) {
int[] removed = null;
parser.nextToken();
while (parser.nextToken() != JsonToken.END_OBJECT) {
String argname = parser.getCurrentName();
parser.nextToken();
if (argname.equals("torrents")) {
((TorrentGetResponse) response).setTorrentStatus(
dataSource.updateTorrents(
profile.getId(), parser,
((TorrentGetResponse) response).getRemoveObsolete()
)
);
} else if (argname.equals("removed")) {
removed = mapper.readValue(parser, int[].class);
}
}
if (removed != null && removed.length > 0) {
if (dataSource.removeTorrents(profile.getId(), removed)) {
((TorrentGetResponse) response).getTorrentStatus().hasRemoved = true;
}
}
} else if (response.getClass() == AddTorrentResponse.class) {
parser.nextToken();
while (parser.nextToken() != JsonToken.END_OBJECT) {
String argname = parser.getCurrentName();
parser.nextValue();
if (argname.equals("torrent-added")) {
int id = -1;
String addedName = null;
String addedHash = null;
while (parser.nextToken() != JsonToken.END_OBJECT) {
String key = parser.getCurrentName();
parser.nextValue();
switch (key) {
case "id":
id = parser.getIntValue();
((AddTorrentResponse) response).setAddedId(id);
break;
case "name":
addedName = parser.getText();
break;
case "hashString":
addedHash = parser.getText();
((AddTorrentResponse) response).setAddedHash(addedHash);
break;
}
}
dataSource.addTorrent(profile.getId(), id, addedName, addedHash,
((AddTorrentResponse) response).getLocation());
} else if (argname.equals("torrent-duplicate")) {
while (parser.nextToken() != JsonToken.END_OBJECT) {
String key = parser.getCurrentName();
parser.nextValue();
if (key.equals("id")) {
((AddTorrentResponse) response).setDuplicateId(parser.getIntValue());
}
}
}
}
} else if (response.getClass() == PortTestResponse.class) {
parser.nextToken();
while (parser.nextToken() != JsonToken.END_OBJECT) {
String argname = parser.getCurrentName();
if (argname.equals("port-is-open")) {
boolean isOpen = parser.nextBooleanValue();
((PortTestResponse) response).setPortOpen(isOpen);
} else {
parser.nextToken();
}
}
} else if (response.getClass() == BlocklistUpdateResponse.class) {
parser.nextToken();
while (parser.nextToken() != JsonToken.END_OBJECT) {
String argname = parser.getCurrentName();
if (argname.equals("blocklist-size")) {
long size = parser.nextLongValue(0);
((BlocklistUpdateResponse) response).setBlocklistSize(size);
} else {
parser.nextToken();
}
}
} else if (response.getClass() == FreeSpaceResponse.class) {
parser.nextToken();
while (parser.nextToken() != JsonToken.END_OBJECT) {
String argname = parser.getCurrentName();
switch (argname) {
case "size-bytes":
long size = parser.nextLongValue(0);
((FreeSpaceResponse) response).setFreeSpace(size);
break;
case "path":
String path = parser.nextTextValue();
((FreeSpaceResponse) response).setPath(path);
break;
default:
parser.nextToken();
break;
}
}
} else {
parser.skipChildren();
}
break;
default:
parser.nextToken();
break;
}
}
}
public static class Response {
protected String result = null;
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
public static class SessionGetResponse extends Response {}
public static class TorrentGetResponse extends Response {
private TorrentStatus status;
private boolean removeObsolete;
public TorrentStatus getTorrentStatus() {
return status;
}
public void setTorrentStatus(TorrentStatus status) {
this.status = status;
}
public boolean getRemoveObsolete() {
return removeObsolete;
}
public void setRemoveObsolete(boolean remove) {
removeObsolete = remove;
}
}
public static class AddTorrentResponse extends Response {
private int addedId = -1;
private String addedHash;
private int duplicateId = -1;
private String location;
public int getAddedId() {
return addedId;
}
public void setAddedId(int id) {
addedId = id;
}
public String getAddedHash() {
return addedHash;
}
public void setAddedHash(String hash) {
addedHash = hash;
}
public boolean isDuplicate() {
return duplicateId != -1;
}
public void setDuplicateId(int id) {
duplicateId = id;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
public static class PortTestResponse extends Response {
public boolean open;
public boolean isPortOpen() {
return open;
}
public void setPortOpen(boolean isOpen) {
this.open = isOpen;
}
}
public static class BlocklistUpdateResponse extends Response {
public long size;
public long getBlocklistSize() {
return size;
}
public void setBlocklistSize(long size) {
this.size = size;
}
}
public static class FreeSpaceResponse extends Response {
public long freeSpace;
public String path;
public long getFreeSpace() {
return freeSpace;
}
public void setFreeSpace(long size) {
this.freeSpace = size;
}
public void setPath(String path) {
this.path = path;
}
}
public static int[] convertIntegerList(List<Integer> list) {
int[] ret = new int[list.size()];
Iterator<Integer> iterator = list.iterator();
for (int i = 0; i < ret.length; i++) {
ret[i] = iterator.next();
}
return ret;
}
public static String[] convertStringList(List<String> list) {
String[] ret = new String[list.size()];
for (int i = 0; i < ret.length; i++) {
ret[i] = list.get(i);
}
return ret;
}
}