From e54cc6e1a0769f81e2570d6fe6e2a205f1a2afe9 Mon Sep 17 00:00:00 2001 From: Alon Albert Date: Mon, 1 Jan 2018 07:12:27 +0200 Subject: [PATCH] Initial connit --- app/src/main/java/deluge/Util.java | 50 + .../main/java/deluge/api/DelugeException.java | 30 + .../main/java/deluge/api/DelugeFuture.java | 78 ++ .../java/deluge/api/ResponseCallback.java | 16 + .../deluge/api/response/IntegerResponse.java | 20 + .../java/deluge/api/response/Response.java | 62 ++ .../java/deluge/api/response/ReturnType.java | 6 + .../api/response/TorrentsStatusResponse.java | 23 + .../main/java/deluge/impl/AsyncResponse.java | 58 ++ .../main/java/deluge/impl/DataHandler.java | 81 ++ .../main/java/deluge/impl/DelugeClient.java | 24 + .../main/java/deluge/impl/DelugeSession.java | 100 ++ .../main/java/deluge/impl/OngoingRequest.java | 26 + .../java/deluge/impl/OngoingRequests.java | 23 + app/src/main/java/deluge/impl/Request.java | 56 ++ .../main/java/deluge/impl/RequestFactory.java | 44 + app/src/main/java/deluge/impl/Util.java | 16 + .../impl/net/AcceptAllTrustManager.java | 23 + .../deluge/impl/net/ResponseExecutor.java | 32 + .../main/java/deluge/impl/net/SSL3Socket.java | 41 + .../main/java/deluge/impl/net/Session.java | 245 +++++ .../java/deluge/impl/net/TorrentField.java | 34 + .../java/org/transdroid/daemon/Daemon.java | 43 +- .../daemon/Deluge/DelugeDirectAdapter.java | 876 ++++++++++++++++++ .../java/se/dimovski/rencode/Rencode.java | 32 + .../dimovski/rencode/RencodeInputStream.java | 494 ++++++++++ .../dimovski/rencode/RencodeOutputStream.java | 404 ++++++++ .../java/se/dimovski/rencode/TypeCode.java | 47 + .../main/java/se/dimovski/rencode/Utils.java | 74 ++ app/src/main/res/values/strings.xml | 2 + 30 files changed, 3046 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/deluge/Util.java create mode 100644 app/src/main/java/deluge/api/DelugeException.java create mode 100644 app/src/main/java/deluge/api/DelugeFuture.java create mode 100644 app/src/main/java/deluge/api/ResponseCallback.java create mode 100644 app/src/main/java/deluge/api/response/IntegerResponse.java create mode 100644 app/src/main/java/deluge/api/response/Response.java create mode 100644 app/src/main/java/deluge/api/response/ReturnType.java create mode 100644 app/src/main/java/deluge/api/response/TorrentsStatusResponse.java create mode 100644 app/src/main/java/deluge/impl/AsyncResponse.java create mode 100644 app/src/main/java/deluge/impl/DataHandler.java create mode 100644 app/src/main/java/deluge/impl/DelugeClient.java create mode 100644 app/src/main/java/deluge/impl/DelugeSession.java create mode 100644 app/src/main/java/deluge/impl/OngoingRequest.java create mode 100644 app/src/main/java/deluge/impl/OngoingRequests.java create mode 100644 app/src/main/java/deluge/impl/Request.java create mode 100644 app/src/main/java/deluge/impl/RequestFactory.java create mode 100644 app/src/main/java/deluge/impl/Util.java create mode 100644 app/src/main/java/deluge/impl/net/AcceptAllTrustManager.java create mode 100644 app/src/main/java/deluge/impl/net/ResponseExecutor.java create mode 100644 app/src/main/java/deluge/impl/net/SSL3Socket.java create mode 100644 app/src/main/java/deluge/impl/net/Session.java create mode 100644 app/src/main/java/deluge/impl/net/TorrentField.java create mode 100644 app/src/main/java/org/transdroid/daemon/Deluge/DelugeDirectAdapter.java create mode 100644 app/src/main/java/se/dimovski/rencode/Rencode.java create mode 100644 app/src/main/java/se/dimovski/rencode/RencodeInputStream.java create mode 100644 app/src/main/java/se/dimovski/rencode/RencodeOutputStream.java create mode 100644 app/src/main/java/se/dimovski/rencode/TypeCode.java create mode 100644 app/src/main/java/se/dimovski/rencode/Utils.java diff --git a/app/src/main/java/deluge/Util.java b/app/src/main/java/deluge/Util.java new file mode 100644 index 00000000..7e1e5039 --- /dev/null +++ b/app/src/main/java/deluge/Util.java @@ -0,0 +1,50 @@ +package deluge; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Util +{ + + public static void close(Closeable closeable) throws IOException + { + if (closeable == null) + { + return; + } + closeable.close(); + } + + public static long copy(InputStream from, OutputStream to) throws IOException + { + try + { + try + { + byte[] buf = new byte[4000]; + long total = 0; + while (true) + { + int r = from.read(buf); + if (r == -1) + { + break; + } + to.write(buf, 0, r); + total += r; + } + return total; + } + finally + { + Util.close(to); + } + } + finally + { + Util.close(from); + } + } +} diff --git a/app/src/main/java/deluge/api/DelugeException.java b/app/src/main/java/deluge/api/DelugeException.java new file mode 100644 index 00000000..f27bdc09 --- /dev/null +++ b/app/src/main/java/deluge/api/DelugeException.java @@ -0,0 +1,30 @@ +package deluge.api; + +public class DelugeException extends Exception +{ + private static final long serialVersionUID = 1L; + + final public String exceptionType; + final public String exceptionMsg; + final public String traceback; + + public DelugeException(String type, String msg, String trace) + { + this.exceptionType = type; + this.exceptionMsg = msg; + this.traceback = trace; + } + + @Override + public void printStackTrace() + { + System.err.println(toString()); + System.err.println(this.traceback); + } + + @Override + public String toString() + { + return DelugeException.class.getCanonicalName() + " " + this.exceptionType + " (" + this.exceptionMsg + ")"; + } +} diff --git a/app/src/main/java/deluge/api/DelugeFuture.java b/app/src/main/java/deluge/api/DelugeFuture.java new file mode 100644 index 00000000..d9eaca74 --- /dev/null +++ b/app/src/main/java/deluge/api/DelugeFuture.java @@ -0,0 +1,78 @@ +package deluge.api; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import deluge.impl.AsyncResponse; + +public class DelugeFuture extends AsyncResponse implements Future +{ + private final CountDownLatch latch = new CountDownLatch(1); + private V value; + + public DelugeFuture() + { + } + + public boolean cancel(boolean mayInterruptIfRunning) + { + // TODO Auto-generated method stub + return false; + } + + public V get() throws InterruptedException, ExecutionException + { + this.latch.await(); + return this.value; + } + + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + if (!this.latch.await(timeout, unit)) + { + throw new TimeoutException(); + } + return this.value; + } + + public boolean isCancelled() + { + // TODO Auto-generated method stub + return false; + } + + public boolean isDone() + { + return this.value != null; + } + + @Override + public void onError(DelugeException error) + { + setValue(null); + super.onError(error); + } + + @Override + public void onResponse(V response) + { + setValue(response); + super.onResponse(response); + } + + @Override + public void onServerError(Exception exception) + { + setValue(null); + super.onServerError(exception); + } + + public void setValue(V val) + { + this.value = val; + this.latch.countDown(); + } +} diff --git a/app/src/main/java/deluge/api/ResponseCallback.java b/app/src/main/java/deluge/api/ResponseCallback.java new file mode 100644 index 00000000..bb00f989 --- /dev/null +++ b/app/src/main/java/deluge/api/ResponseCallback.java @@ -0,0 +1,16 @@ +package deluge.api; + +public abstract class ResponseCallback +{ + public void onError(E error) + { + error.printStackTrace(); + } + + public abstract void onResponse(R response); + + public void onServerError(Exception exception) + { + exception.printStackTrace(); + } +} diff --git a/app/src/main/java/deluge/api/response/IntegerResponse.java b/app/src/main/java/deluge/api/response/IntegerResponse.java new file mode 100644 index 00000000..b68d7775 --- /dev/null +++ b/app/src/main/java/deluge/api/response/IntegerResponse.java @@ -0,0 +1,20 @@ +package deluge.api.response; + +import java.io.IOException; +import java.util.List; + +import deluge.api.DelugeException; + +public class IntegerResponse extends Response +{ + + public IntegerResponse(List data) throws IOException, DelugeException + { + super(data); + } + + public Integer getReturnValue() + { + return (Integer) this.returnValue.get(2); + } +} diff --git a/app/src/main/java/deluge/api/response/Response.java b/app/src/main/java/deluge/api/response/Response.java new file mode 100644 index 00000000..2ad8a51e --- /dev/null +++ b/app/src/main/java/deluge/api/response/Response.java @@ -0,0 +1,62 @@ +package deluge.api.response; + +import java.util.List; + +import deluge.api.DelugeException; + +public abstract class Response +{ + protected List returnValue; + + protected final int RPC_RESPONSE = 1; + protected final int RPC_ERROR = 2; + protected final int RPC_EVENT = 3; + + public Response(List decodedObj) throws DelugeException + { + rawData(decodedObj); + } + + public int getMessageType() + { + return (Integer) this.returnValue.get(0); + } + + public int getRequestId() + { + return (Integer) this.returnValue.get(1); + } + + public abstract Object getReturnValue(); + + private void process() throws DelugeException + { + if (getMessageType() == this.RPC_ERROR) + { + @SuppressWarnings("unchecked") + final List params = (List) this.returnValue.get(2); + final String type = params.get(0); + final String msg = params.get(1); + final String trace = params.get(2); + + throw new DelugeException(type, msg, trace); + } + } + + public void rawData(List decodedObj) throws DelugeException + { + this.returnValue = decodedObj; + process(); + } + + @Override + public String toString() + { + String str = "Response { "; + + str += this.returnValue.toString(); + + str += " }"; + return str; + } +} diff --git a/app/src/main/java/deluge/api/response/ReturnType.java b/app/src/main/java/deluge/api/response/ReturnType.java new file mode 100644 index 00000000..31883ba7 --- /dev/null +++ b/app/src/main/java/deluge/api/response/ReturnType.java @@ -0,0 +1,6 @@ +package deluge.api.response; + +public enum ReturnType +{ + INTEGER, TORRENTS_STATUS +} diff --git a/app/src/main/java/deluge/api/response/TorrentsStatusResponse.java b/app/src/main/java/deluge/api/response/TorrentsStatusResponse.java new file mode 100644 index 00000000..c07b7033 --- /dev/null +++ b/app/src/main/java/deluge/api/response/TorrentsStatusResponse.java @@ -0,0 +1,23 @@ +package deluge.api.response; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import deluge.api.DelugeException; + +public class TorrentsStatusResponse extends Response +{ + + public TorrentsStatusResponse(List data) throws IOException, DelugeException + { + super(data); + } + + @Override + @SuppressWarnings("unchecked") + public Map> getReturnValue() + { + return (Map>) this.returnValue.get(2); + } +} diff --git a/app/src/main/java/deluge/impl/AsyncResponse.java b/app/src/main/java/deluge/impl/AsyncResponse.java new file mode 100644 index 00000000..0c1552cf --- /dev/null +++ b/app/src/main/java/deluge/impl/AsyncResponse.java @@ -0,0 +1,58 @@ +package deluge.impl; + +import java.util.ArrayList; +import java.util.List; + +import deluge.api.ResponseCallback; + +public class AsyncResponse extends ResponseCallback +{ + private final List> callbacks; + + public AsyncResponse() + { + this.callbacks = new ArrayList>(); + } + + public void addCallback(ResponseCallback callback) + { + this.callbacks.add(callback); + } + + @Override + public void onError(E error) + { + for (final ResponseCallback cb : this.callbacks) + { + cb.onError(error); + } + } + + @Override + public void onResponse(R response) + { + for (final ResponseCallback cb : this.callbacks) + { + cb.onResponse(response); + } + } + + @Override + public void onServerError(Exception exception) + { + for (final ResponseCallback cb : this.callbacks) + { + cb.onServerError(exception); + } + } + + public void removeCallback(ResponseCallback callback) + { + this.callbacks.remove(callback); + } + + public void then(ResponseCallback callback) + { + addCallback(callback); + } +} diff --git a/app/src/main/java/deluge/impl/DataHandler.java b/app/src/main/java/deluge/impl/DataHandler.java new file mode 100644 index 00000000..3ac5f37d --- /dev/null +++ b/app/src/main/java/deluge/impl/DataHandler.java @@ -0,0 +1,81 @@ +package deluge.impl; + +import java.io.IOException; +import java.util.List; + +import se.dimovski.rencode.Rencode; +import deluge.api.DelugeException; +import deluge.api.DelugeFuture; +import deluge.api.response.IntegerResponse; +import deluge.api.response.Response; +import deluge.api.response.TorrentsStatusResponse; +import deluge.impl.net.Session.DataCallback; + +public class DataHandler implements DataCallback +{ + + @SuppressWarnings("unchecked") + public void dataRecived(byte[] data) + { + Integer requestId = null; + List decodedObj; + try + { + decodedObj = (List) Rencode.decode(data); + requestId = (Integer) decodedObj.get(1); + + sendSpecificResponse(requestId, decodedObj); + } + catch (final IOException e) + { + e.printStackTrace(); + } + } + + @SuppressWarnings("unchecked") + private void sendSpecificResponse(Integer requestId, List decodedObj) + { + + final OngoingRequest req = OngoingRequests.remove(requestId); + + try + { + switch (req.getType()) + { + case INTEGER: + { + final DelugeFuture fut = (DelugeFuture) req.getFuture(); + fut.onResponse(new IntegerResponse(decodedObj)); + } + break; + case TORRENTS_STATUS: + { + final DelugeFuture fut = (DelugeFuture) req + .getFuture(); + fut.onResponse(new TorrentsStatusResponse(decodedObj)); + } + break; + default: + { + throw new UnsupportedOperationException("Unknown Request: " + req.getType()); + } + } + } + catch (final DelugeException e) + { + final DelugeFuture fut = (DelugeFuture) req.getFuture(); + fut.onError(e); + } + catch (final Exception e) + { + final DelugeFuture fut = (DelugeFuture) req.getFuture(); + fut.onServerError(e); + } + } + + private void onError() + { + + } + +} diff --git a/app/src/main/java/deluge/impl/DelugeClient.java b/app/src/main/java/deluge/impl/DelugeClient.java new file mode 100644 index 00000000..9f03cc13 --- /dev/null +++ b/app/src/main/java/deluge/impl/DelugeClient.java @@ -0,0 +1,24 @@ +package deluge.impl; + +public class DelugeClient +{ + public static DelugeSession getSession(String host) + { + final String[] parts = host.split(":"); + final int port = parts.length < 2 ? DelugeClient.DEFAULT_PORT : Integer.parseInt(parts[1]); + return DelugeClient.getSession(parts[0], port); + } + + public static DelugeSession getSession(String host, int port) + { + return DelugeSession.connect(host, port); + } + + public static DelugeSession getSessionDefault() + { + return DelugeClient.getSession("127.0.0.1", DelugeClient.DEFAULT_PORT); + } + + public static final int DEFAULT_PORT = 58846; + +} diff --git a/app/src/main/java/deluge/impl/DelugeSession.java b/app/src/main/java/deluge/impl/DelugeSession.java new file mode 100644 index 00000000..486a8394 --- /dev/null +++ b/app/src/main/java/deluge/impl/DelugeSession.java @@ -0,0 +1,100 @@ +package deluge.impl; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import deluge.api.DelugeFuture; +import deluge.api.response.IntegerResponse; +import deluge.api.response.ReturnType; +import deluge.api.response.TorrentsStatusResponse; +import deluge.impl.net.Session; +import deluge.impl.net.TorrentField; + +public class DelugeSession +{ + public static DelugeSession connect(String host, int port) + { + final Session session = new Session(host, port); + try + { + session.listen(new DataHandler()); + } + catch (final IOException e) + { + e.printStackTrace(); + } + return new DelugeSession(session); + } + + private final Session session; + + private DelugeSession(Session session) + + { + this.session = session; + } + + public DelugeFuture getTorrentsStatus(Map filter, TorrentField[] fields) + { + final DelugeFuture future = new DelugeFuture(); + final Request request = RequestFactory.getTorrentsStatus(filter, fields); + send(request, ReturnType.TORRENTS_STATUS, future); + return future; + } + + public DelugeFuture login(String username, String password) + { + final DelugeFuture future = new DelugeFuture(); + final Request request = new Request("daemon.login", Util.objects(username, password)); + send(request, ReturnType.INTEGER, future); + return future; + } + + public DelugeFuture pauseTorrent(List torrentIds) + { + final DelugeFuture future = new DelugeFuture(); + final Request request = new Request("core.pause_torrent", Util.objects(torrentIds)); + send(request, ReturnType.INTEGER, future); + return future; + } + + public DelugeFuture resumeTorrent(List torrentIds) + { + final DelugeFuture future = new DelugeFuture(); + final Request request = new Request("core.resume_torrent", Util.objects(torrentIds)); + send(request, ReturnType.INTEGER, future); + return future; + } + + public DelugeFuture addTorrentFile(String name, String encodedContents, Map options) + { + final DelugeFuture future = new DelugeFuture(); + Request request = new Request("core.add_torrent_file", Util.objects(name, encodedContents, options)); + send(request, ReturnType.INTEGER, future); + return future; + } + + public DelugeFuture removeTorrent(String torrentId, Boolean removeData) + { + final DelugeFuture future = new DelugeFuture(); + Request request = new Request("core.remove_torrent", Util.objects(torrentId, removeData)); + send(request, ReturnType.INTEGER, future); + return future; + } + + private void send(Request request, ReturnType type, Object future) + { + OngoingRequests.put(request.getRequestId(), type, future); + + try + { + this.session.send(request.toByteArray()); + } + catch (final IOException e) + { + e.printStackTrace(); + } + } + +} diff --git a/app/src/main/java/deluge/impl/OngoingRequest.java b/app/src/main/java/deluge/impl/OngoingRequest.java new file mode 100644 index 00000000..1374ff8e --- /dev/null +++ b/app/src/main/java/deluge/impl/OngoingRequest.java @@ -0,0 +1,26 @@ +package deluge.impl; + +import deluge.api.response.ReturnType; + +public class OngoingRequest +{ + private final ReturnType type; + private final Object future; + + public OngoingRequest(ReturnType type, Object future) + { + this.type = type; + this.future = future; + } + + public Object getFuture() + { + return this.future; + } + + public ReturnType getType() + { + return this.type; + } + +} diff --git a/app/src/main/java/deluge/impl/OngoingRequests.java b/app/src/main/java/deluge/impl/OngoingRequests.java new file mode 100644 index 00000000..c6443343 --- /dev/null +++ b/app/src/main/java/deluge/impl/OngoingRequests.java @@ -0,0 +1,23 @@ +package deluge.impl; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import deluge.api.response.ReturnType; + +public class OngoingRequests +{ + public static void put(int requestId, ReturnType type, Object future) + { + final OngoingRequest ongoing = new OngoingRequest(type, future); + OngoingRequests.mOngoingRequests.put(requestId, ongoing); + } + + public static OngoingRequest remove(int requestId) + { + return OngoingRequests.mOngoingRequests.remove(requestId); + } + + private static Map mOngoingRequests = new ConcurrentHashMap(); + +} diff --git a/app/src/main/java/deluge/impl/Request.java b/app/src/main/java/deluge/impl/Request.java new file mode 100644 index 00000000..12933bf3 --- /dev/null +++ b/app/src/main/java/deluge/impl/Request.java @@ -0,0 +1,56 @@ +package deluge.impl; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import se.dimovski.rencode.Rencode; + +public class Request +{ + private static AtomicInteger requestCounter = new AtomicInteger(); + + private final Integer requestId; + private final String method; + private final Object[] args; + private final Map kwargs; + + public Request(String method) + { + this(method, new Object[0]); + } + + public Request(String method, Object[] args) + { + this(method, args, new HashMap()); + } + + public Request(String method, Object[] args, Map kwargs) + { + this.requestId = Request.requestCounter.getAndIncrement(); + this.method = method; + this.args = args; + this.kwargs = kwargs; + } + + public Integer getRequestId() + { + return this.requestId; + } + + public byte[] toByteArray() + { + final Object obj = new Object[] { new Object[] { this.requestId, this.method, this.args, this.kwargs } }; + try + { + return Rencode.encode(obj); + } + catch (final IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/deluge/impl/RequestFactory.java b/app/src/main/java/deluge/impl/RequestFactory.java new file mode 100644 index 00000000..89272a68 --- /dev/null +++ b/app/src/main/java/deluge/impl/RequestFactory.java @@ -0,0 +1,44 @@ +package deluge.impl; + +import java.util.Map; + +import deluge.impl.net.TorrentField; + +public class RequestFactory +{ + + public static Request getSessionState() + { + return new Request("core.get_session_state"); + } + + public static Request getTorrentsStatus() + { + return RequestFactory.getTorrentsStatus(null, null); + } + + public static Request getTorrentsStatus(Map filter) + { + return RequestFactory.getTorrentsStatus(filter, null); + } + + public static Request getTorrentsStatus(Map filter, TorrentField[] fields) + { + Object[] fieldNames = new Object[0]; + if (fields != null) + { + fieldNames = new Object[fields.length]; + for (int i = 0; i < fields.length; i++) + { + fieldNames[i] = fields[i].toString(); + } + } + + return new Request("core.get_torrents_status", Util.objects(filter, fieldNames)); + } + + public static Request getTorrentsStatus(TorrentField[] fields) + { + return RequestFactory.getTorrentsStatus(null, null); + } +} diff --git a/app/src/main/java/deluge/impl/Util.java b/app/src/main/java/deluge/impl/Util.java new file mode 100644 index 00000000..e02c24ec --- /dev/null +++ b/app/src/main/java/deluge/impl/Util.java @@ -0,0 +1,16 @@ +package deluge.impl; + +import deluge.impl.net.TorrentField; + +public class Util +{ + public static TorrentField[] fields(TorrentField... fields) + { + return fields; + } + + public static Object[] objects(Object... objects) + { + return objects; + } +} diff --git a/app/src/main/java/deluge/impl/net/AcceptAllTrustManager.java b/app/src/main/java/deluge/impl/net/AcceptAllTrustManager.java new file mode 100644 index 00000000..ee3ec118 --- /dev/null +++ b/app/src/main/java/deluge/impl/net/AcceptAllTrustManager.java @@ -0,0 +1,23 @@ +package deluge.impl.net; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + +public class AcceptAllTrustManager implements X509TrustManager +{ + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException + { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException + { + } + + public X509Certificate[] getAcceptedIssuers() + { + return null; + } +} diff --git a/app/src/main/java/deluge/impl/net/ResponseExecutor.java b/app/src/main/java/deluge/impl/net/ResponseExecutor.java new file mode 100644 index 00000000..b55c2552 --- /dev/null +++ b/app/src/main/java/deluge/impl/net/ResponseExecutor.java @@ -0,0 +1,32 @@ +package deluge.impl.net; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ResponseExecutor +{ + + ExecutorService mExecutor; + + public ResponseExecutor() + { + this.mExecutor = Executors.newFixedThreadPool(20); + } + + public void execute(Runnable task) + { + this.mExecutor.execute(task); + } + + public void shutdown() + { + if (this.mExecutor != null) + { + this.mExecutor.shutdown(); + while (!this.mExecutor.isTerminated()) + { + } + System.out.println("Finished all threads"); + } + } +} diff --git a/app/src/main/java/deluge/impl/net/SSL3Socket.java b/app/src/main/java/deluge/impl/net/SSL3Socket.java new file mode 100644 index 00000000..9930898e --- /dev/null +++ b/app/src/main/java/deluge/impl/net/SSL3Socket.java @@ -0,0 +1,41 @@ +package deluge.impl.net; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HandshakeCompletedEvent; +import javax.net.ssl.HandshakeCompletedListener; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; + +public class SSL3Socket +{ + + public static SSLSocket createSSLv3Socket(String address, int port) throws KeyManagementException, + UnknownHostException, IOException, NoSuchAlgorithmException + { + final TrustManager[] trustAllCerts = new TrustManager[] { new AcceptAllTrustManager() }; + + final SSLContext sc = SSLContext.getInstance("SSLv3"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + + final SSLSocket mySocket = (SSLSocket) sc.getSocketFactory().createSocket(address, port); + + final String[] protocols = { "SSLv3", "TLSv1" }; + mySocket.setEnabledProtocols(protocols); + + mySocket.addHandshakeCompletedListener(new HandshakeCompletedListener() + { + + public void handshakeCompleted(HandshakeCompletedEvent event) + { + System.out.println("Handshake complete"); + } + }); + + return mySocket; + } +} diff --git a/app/src/main/java/deluge/impl/net/Session.java b/app/src/main/java/deluge/impl/net/Session.java new file mode 100644 index 00000000..511660cf --- /dev/null +++ b/app/src/main/java/deluge/impl/net/Session.java @@ -0,0 +1,245 @@ +package deluge.impl.net; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import javax.net.ssl.SSLSocket; + +import deluge.Util; + +public class Session +{ + public interface DataCallback + { + public void dataRecived(byte[] data); + } + + public static byte[] decompressByteArray(byte[] data) throws IOException + { + final InputStream from = new InflaterInputStream(new ByteArrayInputStream(data)); + final ByteArrayOutputStream to = new ByteArrayOutputStream(); + Util.copy(from, to); + byte[] output = to.toByteArray(); + from.close(); + to.close(); + return output; + } + + private static byte[] decompress(byte[] input) throws DataFormatException + { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + final Inflater decompresser = new Inflater(false); + + decompresser.setInput(input, 0, input.length); + final byte[] result = new byte[1024]; + while (!decompresser.finished()) + { + final int resultLength = decompresser.inflate(result); + baos.write(result, 0, resultLength); + } + decompresser.end(); + + final byte[] returnValue = baos.toByteArray(); + try + { + baos.close(); + } + catch (final IOException e) + { + } + return returnValue; + } + + private final BlockingQueue queue = new ArrayBlockingQueue(50); + private SSLSocket mySocket; + String myAddress; + int myPort; + Thread sender = null; + + public final CountDownLatch latch = new CountDownLatch(1); + + public Session(String address, int port) + { + this.myAddress = address; + this.myPort = port; + } + + public byte[] compress(byte[] data) throws IOException + { + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + final Deflater d = new Deflater(); + final DeflaterOutputStream dout = new DeflaterOutputStream(baos, d); + dout.write(data); + dout.close(); + + final byte[] output = baos.toByteArray(); + baos.close(); + return output; + } + + private void createSocket() + { + if (this.mySocket == null) + { + try + { + this.mySocket = SSL3Socket.createSSLv3Socket(this.myAddress, this.myPort); + this.mySocket.startHandshake(); + } + catch (final Exception e1) + { + e1.printStackTrace(); + this.mySocket = null; + } + } + this.latch.countDown(); + } + + public void listen(final DataCallback cb) throws IOException + { + new Thread(new Runnable() + { + + public void run() + { + createSocket(); + System.out.println("Listening Thread started"); + try + { + while (Session.this.mySocket != null) + { + + final InputStream inputStream = new BufferedInputStream(Session.this.mySocket.getInputStream()); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + int bytesRead; + final byte[] buffer = new byte[1024]; + while ((bytesRead = inputStream.read(buffer)) != -1) + { + baos.write(buffer); + + if (bytesRead < 1024) + { + final byte[] unpacked = Session.decompressByteArray(baos.toByteArray()); + baos.reset(); + cb.dataRecived(unpacked); + } + } + } + } + catch (final UnsupportedEncodingException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + catch (final IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + }).start(); + + try + { + this.latch.await(3, TimeUnit.SECONDS); + } + catch (final InterruptedException e) + { + } + if (this.mySocket == null) + { + throw new IOException(); + } + } + + public void send(byte[] request) throws IOException + { + if (this.sender == null) + { + sender(); + } + try + { + this.queue.put(request); + } + catch (final InterruptedException e) + { + e.printStackTrace(); + } + } + + public void sender() throws IOException + { + this.sender = new Thread(new Runnable() + { + + public void run() + { + createSocket(); + System.out.println("Sending Thread started"); + try + { + while (Session.this.mySocket != null) + { + byte[] packedData; + try + { + final byte[] x = Session.this.queue.take(); + packedData = compress(x); + + final OutputStream out = new BufferedOutputStream(Session.this.mySocket.getOutputStream()); + out.write(packedData); + out.flush(); + } + catch (final InterruptedException e) + { + e.printStackTrace(); + } + } + + } + catch (final IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + }); + this.sender.start(); + + try + { + this.latch.await(3, TimeUnit.SECONDS); + } + catch (final InterruptedException e) + { + } + if (this.mySocket == null) + { + throw new IOException(); + } + } + +} diff --git a/app/src/main/java/deluge/impl/net/TorrentField.java b/app/src/main/java/deluge/impl/net/TorrentField.java new file mode 100644 index 00000000..2313de69 --- /dev/null +++ b/app/src/main/java/deluge/impl/net/TorrentField.java @@ -0,0 +1,34 @@ +package deluge.impl.net; + +public enum TorrentField +{ + ACTIVE_TIME("active_time"), ALL_TIME_DOWNLOAD("all_time_download"), COMPACT("compact"), DISTRIBUTED_COPIES( + "distributed_copies"), DOWNLOAD_PAYLOAD_RATE("download_payload_rate"), FILE_PRIORITIES("file_priorities"), HASH( + "hash"), IS_AUTO_MANAGED("is_auto_managed"), IS_FINISHED("is_finished"), MAX_CONNECTIONS("max_connections"), MAX_DOWNLOAD_SPEED( + "max_download_speed"), MAX_UPLOAD_SLOTS("max_upload_slots"), MAX_UPLOAD_SPEED("max_upload_speed"), MESSAGE( + "message"), MOVE_ON_COMPLETED_PATH("move_on_completed_path"), MOVE_ON_COMPLETED("move_on_completed"), MOVE_COMPLETED_PATH( + "move_completed_path"), MOVE_COMPLETED("move_completed"), NEXT_ANNOUNCE("next_announce"), NUM_PEERS( + "num_peers"), NUM_SEEDS("num_seeds"), PAUSED("paused"), PRIORITIZE_FIRST_LAST("prioritize_first_last"), PROGRESS( + "progress"), REMOVE_AT_RATIO("remove_at_ratio"), SAVE_PATH("save_path"), SEEDING_TIME("seeding_time"), SEEDS_PEERS_RATIO( + "seeds_peers_ratio"), SEED_RANK("seed_rank"), STATE("state"), STOP_AT_RATIO("stop_at_ratio"), STOP_RATIO( + "stop_ratio"), TIME_ADDED("time_added"), TOTAL_DONE("total_done"), TOTAL_PAYLOAD_DOWNLOAD( + "total_payload_download"), TOTAL_PAYLOAD_UPLOAD("total_payload_upload"), TOTAL_PEERS("total_peers"), TOTAL_SEEDS( + "total_seeds"), TOTAL_UPLOADED("total_uploaded"), TOTAL_WANTED("total_wanted"), TRACKER("tracker"), TRACKERS( + "trackers"), TRACKER_STATUS("tracker_status"), UPLOAD_PAYLOAD_RATE("upload_payload_rate"), COMMENT( + "comment"), ETA("eta"), FILE_PROGRESS("file_progress"), FILES("files"), IS_SEED("is_seed"), NAME("name"), NUM_FILES( + "num_files"), NUM_PIECES("num_pieces"), PEERS("peers"), PIECE_LENGTH("piece_length"), PRIVATE("private"), QUEUE( + "queue"), RATIO("ratio"), TOTAL_SIZE("total_size"), TRACKER_HOST("tracker_host"), LABEL("label"); + + private final String value; + + TorrentField(String str) + { + this.value = str; + } + + @Override + public String toString() + { + return this.value; + } +} diff --git a/app/src/main/java/org/transdroid/daemon/Daemon.java b/app/src/main/java/org/transdroid/daemon/Daemon.java index e36cff35..6fc9ec00 100644 --- a/app/src/main/java/org/transdroid/daemon/Daemon.java +++ b/app/src/main/java/org/transdroid/daemon/Daemon.java @@ -23,6 +23,7 @@ import org.transdroid.daemon.Bitflu.BitfluAdapter; import org.transdroid.daemon.BuffaloNas.BuffaloNasAdapter; import org.transdroid.daemon.DLinkRouterBT.DLinkRouterBTAdapter; import org.transdroid.daemon.Deluge.DelugeAdapter; +import org.transdroid.daemon.Deluge.DelugeDirectAdapter; import org.transdroid.daemon.Ktorrent.KtorrentAdapter; import org.transdroid.daemon.Qbittorrent.QbittorrentAdapter; import org.transdroid.daemon.Rtorrent.RtorrentAdapter; @@ -66,6 +67,11 @@ public enum Daemon { return new DelugeAdapter(settings); } }, + DelugeDirect { + public IDaemonAdapter createAdapter(DaemonSettings settings) { + return new DelugeDirectAdapter(settings); + } + }, Dummy { public IDaemonAdapter createAdapter(DaemonSettings settings) { return new DummyAdapter(settings); @@ -149,6 +155,8 @@ public enum Daemon { return "daemon_buffalonas"; case Deluge: return "daemon_deluge"; + case DelugeDirect: + return "daemon_deluge_direct"; case DLinkRouterBT: return "daemon_dlinkrouterbt"; case Dummy: @@ -203,6 +211,9 @@ public enum Daemon { if (daemonCode.equals("daemon_deluge")) { return Deluge; } + if (daemonCode.equals("daemon_deluge_direct")) { + return DelugeDirect; + } if (daemonCode.equals("daemon_dlinkrouterbt")) { return DLinkRouterBT; } @@ -262,6 +273,10 @@ public enum Daemon { } case Deluge: return 8112; + + case DelugeDirect: + return 8112; + case Synology: if (ssl) { return 5001; @@ -291,11 +306,11 @@ public enum Daemon { } public static boolean supportsFileListing(Daemon type) { - return type == Synology || type == Transmission || type == uTorrent || type == BitTorrent || type == KTorrent || type == Deluge || type == rTorrent || type == Vuze || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == Aria2 || type == tTorrent || type == Dummy; + return type == Synology || type == Transmission || type == uTorrent || type == BitTorrent || type == KTorrent || type == Deluge || type == DelugeDirect || type == rTorrent || type == Vuze || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == Aria2 || type == tTorrent || type == Dummy; } public static boolean supportsFineDetails(Daemon type) { - return type == uTorrent || type == BitTorrent || type == Daemon.Transmission || type == Deluge || type == rTorrent || type == qBittorrent || type == Aria2 || type == Dummy; + return type == uTorrent || type == BitTorrent || type == Daemon.Transmission || type == Deluge || type == DelugeDirect || type == rTorrent || type == qBittorrent || type == Aria2 || type == Dummy; } public static boolean needsManualPathSpecified(Daemon type) { @@ -303,7 +318,7 @@ public enum Daemon { } public static boolean supportsFilePaths(Daemon type) { - return type == uTorrent || type == BitTorrent || type == Vuze || type == Deluge || type == Transmission || type == rTorrent || type == KTorrent || type == BuffaloNas || type == Aria2 || type == Dummy; + return type == uTorrent || type == BitTorrent || type == Vuze || type == Deluge || type == DelugeDirect || type == Transmission || type == rTorrent || type == KTorrent || type == BuffaloNas || type == Aria2 || type == Dummy; } public static boolean supportsStoppingStarting(Daemon type) { @@ -315,11 +330,11 @@ public enum Daemon { } public static boolean supportsCustomFolder(Daemon type) { - return type == rTorrent || type == Tfb4rt || type == Bitflu || type == Deluge || type == Transmission || type == BitTorrent || type == uTorrent || type == qBittorrent || type == Dummy; + return type == rTorrent || type == Tfb4rt || type == Bitflu || type == Deluge || type == DelugeDirect || type == Transmission || type == BitTorrent || type == uTorrent || type == qBittorrent || type == Dummy; } public static boolean supportsSetTransferRates(Daemon type) { - return type == Deluge || type == Transmission || type == uTorrent || type == BitTorrent || type == rTorrent || type == Vuze || type == BuffaloNas || type == BitComet || type == Aria2 || type == qBittorrent || type == Dummy; + return type == Deluge || type == DelugeDirect || type == Transmission || type == uTorrent || type == BitTorrent || type == rTorrent || type == Vuze || type == BuffaloNas || type == BitComet || type == Aria2 || type == qBittorrent || type == Dummy; } public static boolean supportsAddByFile(Daemon type) { @@ -328,31 +343,31 @@ public enum Daemon { } public static boolean supportsAddByMagnetUrl(Daemon type) { - return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet || type == Aria2 || type == tTorrent || type == Dummy; + return type == uTorrent || type == BitTorrent || type == Transmission || type == Synology || type == Deluge || type == DelugeDirect || type == Bitflu || type == KTorrent || type == rTorrent || type == qBittorrent || type == BitComet || type == Aria2 || type == tTorrent || type == Dummy; } public static boolean supportsRemoveWithData(Daemon type) { - return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == BitTorrent || type == Tfb4rt || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == rTorrent || type == Aria2 || type == tTorrent || type == Dummy; + return type == uTorrent || type == Vuze || type == Transmission || type == Deluge || type == DelugeDirect || type == BitTorrent || type == Tfb4rt || type == DLinkRouterBT || type == Bitflu || type == qBittorrent || type == BuffaloNas || type == BitComet || type == rTorrent || type == Aria2 || type == tTorrent || type == Dummy; } public static boolean supportsFilePrioritySetting(Daemon type) { - return type == BitTorrent || type == uTorrent || type == Transmission || type == KTorrent || type == rTorrent || type == Vuze || type == Deluge || type == qBittorrent || type == tTorrent || type == Dummy; + return type == BitTorrent || type == uTorrent || type == Transmission || type == KTorrent || type == rTorrent || type == Vuze || type == Deluge || type == DelugeDirect || type == qBittorrent || type == tTorrent || type == Dummy; } public static boolean supportsDateAdded(Daemon type) { - return type == Vuze || type == Transmission || type == rTorrent || type == Bitflu || type == BitComet || type == uTorrent || type == BitTorrent || type == Deluge || type == qBittorrent || type == Dummy; + return type == Vuze || type == Transmission || type == rTorrent || type == Bitflu || type == BitComet || type == uTorrent || type == BitTorrent || type == Deluge || type == DelugeDirect || type == qBittorrent || type == Dummy; } public static boolean supportsLabels(Daemon type) { - return type == uTorrent || type == BitTorrent || type == Deluge || type == BitComet || type == rTorrent || type == qBittorrent || type == Dummy; + return type == uTorrent || type == BitTorrent || type == Deluge || type == DelugeDirect || type == BitComet || type == rTorrent || type == qBittorrent || type == Dummy; } public static boolean supportsSetLabel(Daemon type) { - return type == uTorrent || type == BitTorrent || type == rTorrent || type == Deluge || type == qBittorrent || type == Dummy; + return type == uTorrent || type == BitTorrent || type == rTorrent || type == Deluge || type == DelugeDirect || type == qBittorrent || type == Dummy; } public static boolean supportsSetDownloadLocation(Daemon type) { - return type == Transmission || type == Deluge || type == Dummy; + return type == Transmission || type == Deluge || type == DelugeDirect || type == Dummy; } public static boolean supportsSetAlternativeMode(Daemon type) { @@ -360,11 +375,11 @@ public enum Daemon { } public static boolean supportsSetTrackers(Daemon type) { - return type == uTorrent || type == BitTorrent || type == Deluge || type == Dummy; + return type == uTorrent || type == BitTorrent || type == Deluge || type == DelugeDirect || type == Dummy; } public static boolean supportsForceRecheck(Daemon type) { - return type == uTorrent || type == BitTorrent || type == Deluge || type == rTorrent || type == Transmission || type == Dummy || type == qBittorrent; + return type == uTorrent || type == BitTorrent || type == Deluge || type == DelugeDirect || type == rTorrent || type == Transmission || type == Dummy || type == qBittorrent; } public static boolean supportsExtraPassword(Daemon type) { diff --git a/app/src/main/java/org/transdroid/daemon/Deluge/DelugeDirectAdapter.java b/app/src/main/java/org/transdroid/daemon/Deluge/DelugeDirectAdapter.java new file mode 100644 index 00000000..cc85bb4d --- /dev/null +++ b/app/src/main/java/org/transdroid/daemon/Deluge/DelugeDirectAdapter.java @@ -0,0 +1,876 @@ +/* + * This file is part of Transdroid + * + * 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 . + * + */ +package org.transdroid.daemon.Deluge; + +import com.android.internalcopy.http.multipart.FilePart; +import com.android.internalcopy.http.multipart.MultipartEntity; +import com.android.internalcopy.http.multipart.Part; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.cookie.Cookie; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.protocol.HTTP; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.transdroid.core.gui.log.Log; +import org.transdroid.daemon.Daemon; +import org.transdroid.daemon.DaemonException; +import org.transdroid.daemon.DaemonException.ExceptionType; +import org.transdroid.daemon.DaemonSettings; +import org.transdroid.daemon.IDaemonAdapter; +import org.transdroid.daemon.Label; +import org.transdroid.daemon.Priority; +import org.transdroid.daemon.Torrent; +import org.transdroid.daemon.TorrentDetails; +import org.transdroid.daemon.TorrentFile; +import org.transdroid.daemon.TorrentStatus; +import org.transdroid.daemon.task.AddByFileTask; +import org.transdroid.daemon.task.AddByMagnetUrlTask; +import org.transdroid.daemon.task.AddByUrlTask; +import org.transdroid.daemon.task.DaemonTask; +import org.transdroid.daemon.task.DaemonTaskFailureResult; +import org.transdroid.daemon.task.DaemonTaskResult; +import org.transdroid.daemon.task.DaemonTaskSuccessResult; +import org.transdroid.daemon.task.GetFileListTask; +import org.transdroid.daemon.task.GetFileListTaskSuccessResult; +import org.transdroid.daemon.task.GetTorrentDetailsTask; +import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult; +import org.transdroid.daemon.task.PauseTask; +import org.transdroid.daemon.task.RemoveTask; +import org.transdroid.daemon.task.ResumeTask; +import org.transdroid.daemon.task.RetrieveTask; +import org.transdroid.daemon.task.RetrieveTaskSuccessResult; +import org.transdroid.daemon.task.SetDownloadLocationTask; +import org.transdroid.daemon.task.SetFilePriorityTask; +import org.transdroid.daemon.task.SetLabelTask; +import org.transdroid.daemon.task.SetTrackersTask; +import org.transdroid.daemon.task.SetTransferRatesTask; +import org.transdroid.daemon.util.HttpHelper; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import deluge.api.DelugeFuture; +import deluge.api.response.TorrentsStatusResponse; +import deluge.impl.DelugeClient; +import deluge.impl.DelugeSession; +import deluge.impl.net.TorrentField; + +/** + * The daemon adapter from the Deluge torrent client using deluged API directly. + * @author alon.albert + */ +public class DelugeDirectAdapter implements IDaemonAdapter { + + private static final String LOG_NAME = "Deluge daemon"; + + private static final String PATH_TO_RPC = "/json"; + private static final String PATH_TO_UPLOAD = "/upload"; + + private static final String RPC_ID = "id"; + private static final String RPC_METHOD = "method"; + private static final String RPC_PARAMS = "params"; + private static final String RPC_RESULT = "result"; + private static final String RPC_TORRENTS = "torrents"; + private static final String RPC_FILE = "file"; + private static final String RPC_FILES = "files"; + private static final String RPC_SESSION_ID = "_session_id"; + + private static final String RPC_METHOD_LOGIN = "auth.login"; + private static final String RPC_METHOD_GET = "web.update_ui"; + private static final String RPC_METHOD_STATUS = "core.get_torrent_status"; + private static final String RPC_METHOD_ADD = "core.add_torrent_url"; + private static final String RPC_METHOD_ADD_MAGNET = "core.add_torrent_magnet"; + private static final String RPC_METHOD_ADD_FILE = "web.add_torrents"; + private static final String RPC_METHOD_REMOVE = "core.remove_torrent"; + private static final String RPC_METHOD_PAUSE = "core.pause_torrent"; + private static final String RPC_METHOD_PAUSE_ALL = "core.pause_all_torrents"; + private static final String RPC_METHOD_RESUME = "core.resume_torrent"; + private static final String RPC_METHOD_RESUME_ALL = "core.resume_all_torrents"; + private static final String RPC_METHOD_SETCONFIG = "core.set_config"; + private static final String RPC_METHOD_SETFILE = "core.set_torrent_file_priorities"; + //private static final String RPC_METHOD_SETOPTIONS = "core.set_torrent_options"; + private static final String RPC_METHOD_MOVESTORAGE = "core.move_storage"; + private static final String RPC_METHOD_SETTRACKERS = "core.set_torrent_trackers"; + private static final String RPC_METHOD_FORCERECHECK = "core.force_recheck"; + private static final String RPC_METHOD_SETLABEL = "label.set_torrent"; + + private static final String RPC_NAME = "name"; + private static final String RPC_STATUS = "state"; + private static final String RPC_MESSAGE = "message"; + private static final String RPC_SAVEPATH = "save_path"; + private static final String RPC_MAXDOWNLOAD = "max_download_speed"; + private static final String RPC_MAXUPLOAD = "max_upload_speed"; + + private static final String RPC_RATEDOWNLOAD = "download_payload_rate"; + private static final String RPC_RATEUPLOAD = "upload_payload_rate"; + private static final String RPC_NUMSEEDS = "num_seeds"; + private static final String RPC_TOTALSEEDS = "total_seeds"; + private static final String RPC_NUMPEERS = "num_peers"; + private static final String RPC_TOTALPEERS = "total_peers"; + private static final String RPC_ETA = "eta"; + private static final String RPC_TIMEADDED = "time_added"; + + private static final String RPC_DOWNLOADEDEVER = "total_done"; + private static final String RPC_UPLOADEDEVER = "total_uploaded"; + private static final String RPC_TOTALSIZE = "total_size"; + private static final String RPC_PARTDONE = "progress"; + private static final String RPC_LABEL = "label"; + private static final String RPC_TRACKERS = "trackers"; + private static final String RPC_TRACKER_STATUS = "tracker_status"; + private static final TorrentField[] RETREIVE_FIELDS = new TorrentField[] { + TorrentField.NAME, + TorrentField.STATE, + TorrentField.SAVE_PATH, + TorrentField.DOWNLOAD_PAYLOAD_RATE, + TorrentField.UPLOAD_PAYLOAD_RATE, + TorrentField.NUM_PEERS, + TorrentField.NUM_SEEDS, + TorrentField.TOTAL_PEERS, + TorrentField.TOTAL_SEEDS, + TorrentField.ETA, + TorrentField.TOTAL_DONE, + TorrentField.TOTAL_UPLOADED, + TorrentField.TOTAL_SIZE, + TorrentField.PROGRESS, + TorrentField.LABEL, + TorrentField.MESSAGE, + TorrentField.TIME_ADDED, + TorrentField.TRACKER_STATUS, + }; + private static final String[] RPC_FIELDS_ARRAY = + new String[]{RPC_NAME, RPC_STATUS, RPC_SAVEPATH, RPC_RATEDOWNLOAD, RPC_RATEUPLOAD, RPC_NUMPEERS, + RPC_NUMSEEDS, RPC_TOTALPEERS, RPC_TOTALSEEDS, RPC_ETA, RPC_DOWNLOADEDEVER, RPC_UPLOADEDEVER, + RPC_TOTALSIZE, RPC_PARTDONE, RPC_LABEL, RPC_MESSAGE, RPC_TIMEADDED, RPC_TRACKER_STATUS}; + private static final String RPC_DETAILS = "files"; + private static final String RPC_INDEX = "index"; + private static final String RPC_PATH = "path"; + private static final String RPC_SIZE = "size"; + private static final String RPC_FILEPROGRESS = "file_progress"; + private static final String RPC_FILEPRIORITIES = "file_priorities"; + private DaemonSettings settings; + private DefaultHttpClient httpclient; + private DelugeSession delugeSession; + private Cookie sessionCookie; + private int version = -1; + + public DelugeDirectAdapter(DaemonSettings settings) { + this.settings = settings; + } + + public JSONArray addTorrentByFile(String file, Log log) throws JSONException, IOException, DaemonException { + + String url = buildWebUIUrl() + PATH_TO_UPLOAD; + + log.d(LOG_NAME, "Uploading a file to the Deluge daemon: " + url); + + // Initialise the HTTP client + if (httpclient == null) { + initialise(); + } + + // Setup client using POST + HttpPost httppost = new HttpPost(url); + File upload = new File(URI.create(file)); + Part[] parts = {new FilePart(RPC_FILE, upload)}; + httppost.setEntity(new MultipartEntity(parts, httppost.getParams())); + + // Make request + HttpResponse response = httpclient.execute(httppost); + + // Read JSON response + InputStream instream = response.getEntity().getContent(); + String result = HttpHelper.convertStreamToString(instream); + + // If the upload succeeded, add the torrent file on the server + // For this we need the file name, which is now send as a JSON object like: + // {"files": ["/tmp/delugeweb/tmp00000.torrent"], "success": true} + String remoteFile = (new JSONObject(result)).getJSONArray(RPC_FILES).getString(0); + JSONArray params = new JSONArray(); + JSONArray files = new JSONArray(); + JSONObject fileu = new JSONObject(); + fileu.put("path", remoteFile); + fileu.put("options", new JSONArray()); + files.put(fileu); + params.put(files); + + return params; + + } + + @Override + public DaemonTaskResult executeTask(Log log, DaemonTask task) { + + try { + ensureVersion(log); + + JSONArray params = new JSONArray(); + + // Array of the fields needed for files listing calls + JSONArray ffields = new JSONArray(); + ffields.put(RPC_DETAILS); + ffields.put(RPC_FILEPROGRESS); + ffields.put(RPC_FILEPRIORITIES); + + switch (task.getMethod()) { + case Retrieve: + final TorrentsStatusResponse torrents = delugeSession.getTorrentsStatus(null, RETREIVE_FIELDS).get(); + int id = 0; + for (Map.Entry> entry : torrents.getReturnValue().entrySet()) { + final String hash = entry.getKey(); + final Map values = entry.getValue(); + new Torrent( + id, + hash, + (String) values.get(TorrentField.NAME.toString()), + + + + + + ) + } + torrents.get().getReturnValue() + return new RetrieveTaskSuccessResult((RetrieveTask) task, + parseJsonRetrieveTorrents(result.getJSONObject(RPC_RESULT)), + parseJsonRetrieveLabels(result.getJSONObject(RPC_RESULT))); + + case GetTorrentDetails: + + // Array of the fields needed for files listing calls + JSONArray dfields = new JSONArray(); + dfields.put(RPC_TRACKERS); + dfields.put(RPC_TRACKER_STATUS); + + // Request file listing of a torrent + params.put(task.getTargetTorrent().getUniqueID()); // torrent_id + params.put(dfields); // keys + + JSONObject dinfo = makeRequest(buildRequest(RPC_METHOD_STATUS, params), log); + return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, + parseJsonTorrentDetails(dinfo.getJSONObject(RPC_RESULT))); + + case GetFileList: + + // Request file listing of a torrent + params.put(task.getTargetTorrent().getUniqueID()); // torrent_id + params.put(ffields); // keys + + JSONObject finfo = makeRequest(buildRequest(RPC_METHOD_STATUS, params), log); + return new GetFileListTaskSuccessResult((GetFileListTask) task, + parseJsonFileListing(finfo.getJSONObject(RPC_RESULT), task.getTargetTorrent())); + + case AddByFile: + + // Request to add a torrent by local .torrent file + String file = ((AddByFileTask) task).getFile(); + makeRequest(buildRequest(RPC_METHOD_ADD_FILE, addTorrentByFile(file, log)), log); + return new DaemonTaskSuccessResult(task); + + case AddByUrl: + + // Request to add a torrent by URL + String url = ((AddByUrlTask) task).getUrl(); + params.put(url); + params.put(new JSONArray()); + + makeRequest(buildRequest(RPC_METHOD_ADD, params), log); + return new DaemonTaskSuccessResult(task); + + case AddByMagnetUrl: + + // Request to add a magnet link by URL + String magnet = ((AddByMagnetUrlTask) task).getUrl(); + params.put(magnet); + params.put(new JSONArray()); + + makeRequest(buildRequest(RPC_METHOD_ADD_MAGNET, params), log); + return new DaemonTaskSuccessResult(task); + + case Remove: + + // Remove a torrent + RemoveTask removeTask = (RemoveTask) task; + params.put(removeTask.getTargetTorrent().getUniqueID()); + params.put(removeTask.includingData()); + makeRequest(buildRequest(RPC_METHOD_REMOVE, params), log); + return new DaemonTaskSuccessResult(task); + + case Pause: + + // Pause a torrent + PauseTask pauseTask = (PauseTask) task; + makeRequest(buildRequest(RPC_METHOD_PAUSE, ((new JSONArray()) + .put((new JSONArray()).put(pauseTask.getTargetTorrent().getUniqueID())))), log); + return new DaemonTaskSuccessResult(task); + + case PauseAll: + + // Resume all torrents + makeRequest(buildRequest(RPC_METHOD_PAUSE_ALL, null), log); + return new DaemonTaskSuccessResult(task); + + case Resume: + + // Resume a torrent + ResumeTask resumeTask = (ResumeTask) task; + makeRequest(buildRequest(RPC_METHOD_RESUME, ((new JSONArray()) + .put((new JSONArray()).put(resumeTask.getTargetTorrent().getUniqueID())))), log); + return new DaemonTaskSuccessResult(task); + + case ResumeAll: + + // Resume all torrents + makeRequest(buildRequest(RPC_METHOD_RESUME_ALL, null), log); + return new DaemonTaskSuccessResult(task); + + case SetFilePriorities: + + // Set the priorities of files in a specific torrent + SetFilePriorityTask prioTask = (SetFilePriorityTask) task; + + // We first need a listing of all the files (because we can only set the priorities all at once) + params.put(task.getTargetTorrent().getUniqueID()); // torrent_id + params.put(ffields); // keys + JSONObject pinfo = makeRequest(buildRequest(RPC_METHOD_STATUS, params), log); + ArrayList pfiles = + parseJsonFileListing(pinfo.getJSONObject(RPC_RESULT), prioTask.getTargetTorrent()); + + // Now prepare the new list of priorities + params = new JSONArray(); + params.put(task.getTargetTorrent().getUniqueID()); // torrent_id + JSONArray pfields = new JSONArray(); + // Override the priorities in the just retrieved list of all files + for (TorrentFile pfile : pfiles) { + Priority newPriority = pfile.getPriority(); + for (TorrentFile forFile : prioTask.getForFiles()) { + if (forFile.getKey().equals(pfile.getKey())) { + // This is a file that we want to assign a new priority to + newPriority = prioTask.getNewPriority(); + break; + } + } + pfields.put(convertPriority(newPriority)); + } + params.put(pfields); // keys + + // Make a single call to set the priorities on all files at once + makeRequest(buildRequest(RPC_METHOD_SETFILE, params), log); + return new DaemonTaskSuccessResult(task); + + case SetDownloadLocation: + + // Set the download location of some torrent + SetDownloadLocationTask sdlTask = (SetDownloadLocationTask) task; + // This works, but does not move the torrent + //makeRequest(buildRequest(RPC_METHOD_SETOPTIONS, buildSetTorrentOptions( + // sdlTask.getTargetTorrent().getUniqueID(), RPC_DOWNLOADLOCATION, sdlTask.getNewLocation()))); + params.put(new JSONArray().put(task.getTargetTorrent().getUniqueID())); + params.put(sdlTask.getNewLocation()); + makeRequest(buildRequest(RPC_METHOD_MOVESTORAGE, params), log); + return new DaemonTaskSuccessResult(task); + + case SetTransferRates: + + // Request to set the maximum transfer rates + SetTransferRatesTask ratesTask = (SetTransferRatesTask) task; + JSONObject map = new JSONObject(); + map.put(RPC_MAXUPLOAD, (ratesTask.getUploadRate() == null ? -1 : ratesTask.getUploadRate())); + map.put(RPC_MAXDOWNLOAD, (ratesTask.getDownloadRate() == null ? -1 : ratesTask.getDownloadRate())); + + makeRequest(buildRequest(RPC_METHOD_SETCONFIG, (new JSONArray()).put(map)), log); + return new DaemonTaskSuccessResult(task); + + case SetLabel: + + // Request to set the label + SetLabelTask labelTask = (SetLabelTask) task; + params.put(task.getTargetTorrent().getUniqueID()); + params.put(labelTask.getNewLabel() == null ? "" : labelTask.getNewLabel()); + makeRequest(buildRequest(RPC_METHOD_SETLABEL, params), log); + return new DaemonTaskSuccessResult(task); + + case SetTrackers: + + // Set the trackers of some torrent + SetTrackersTask trackersTask = (SetTrackersTask) task; + JSONArray trackers = new JSONArray(); + // Build an JSON arrays of objcts that each have a tier (order) number and an url + for (int i = 0; i < trackersTask.getNewTrackers().size(); i++) { + JSONObject trackerObj = new JSONObject(); + trackerObj.put("tier", i); + trackerObj.put("url", trackersTask.getNewTrackers().get(i)); + trackers.put(trackerObj); + } + params.put(new JSONArray().put(task.getTargetTorrent().getUniqueID())); + params.put(trackers); + makeRequest(buildRequest(RPC_METHOD_SETTRACKERS, params), log); + return new DaemonTaskSuccessResult(task); + + case ForceRecheck: + + // Pause a torrent + makeRequest(buildRequest(RPC_METHOD_FORCERECHECK, + ((new JSONArray()).put((new JSONArray()).put(task.getTargetTorrent().getUniqueID())))), + log); + return new DaemonTaskSuccessResult(task); + + default: + return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, + task.getMethod() + " is not supported by " + getType())); + } + } catch (JSONException e) { + return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ParsingFailed, e.toString())); + } catch (DaemonException e) { + return new DaemonTaskFailureResult(task, e); + } catch (FileNotFoundException e) { + return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString())); + } catch (IOException e) { + return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString())); + } catch (InterruptedException e) { + return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString())); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + /*private JSONArray buildSetTorrentOptions(String torrent, String key, String value) throws JSONException { + JSONArray params = new JSONArray(); + params.put(new JSONArray().put(torrent)); // torrent_id + JSONObject sdlmap = new JSONObject(); + // Set the option setting to the torrent + sdlmap.put(key, value); + params.put(sdlmap); // options + return params; + }*/ + + private void ensureVersion(Log log) throws DaemonException { + if (version > 0) { + return; + } + // We still need to retrieve the version number from the server + // Do this by getting the web interface main html page and trying to parse the version number + // Format is something like 'Deluge: Web UI 1.3.6' + if (httpclient == null) { + initialise(); + } + try { + HttpResponse response = httpclient.execute(new HttpGet(buildWebUIUrl() + "/")); + String main = HttpHelper.convertStreamToString(response.getEntity().getContent()); + String titleStartText = "Deluge: Web UI "; + String titleEndText = ""; + int titleStart = main.indexOf(titleStartText); + int titleEnd = main.indexOf(titleEndText, titleStart); + if (titleStart >= 0 && titleEnd > titleStart) { + // String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .) + String[] parts = main.substring(titleStart + titleStartText.length(), titleEnd).split("\\."); + if (parts.length > 0) { + version = Integer.parseInt(parts[0]) * 100 * 100; + if (parts.length > 1) { + version += Integer.parseInt(parts[1]) * 100; + if (parts.length > 2) { + // For the last part only read until a non-numeric character is read + // For example version 3.0.0-alpha5 is read as version code 30000 + String numbers = ""; + for (char c : parts[2].toCharArray()) { + if (Character.isDigit(c)) + // Still a number; add it to the numbers string + { + numbers += Character.toString(c); + } else { + // No longer reading numbers; stop reading + break; + } + } + version += Integer.parseInt(numbers); + return; + } + } + } + } + } catch (NumberFormatException e) { + log.d(LOG_NAME, "Error parsing the Deluge version code as number: " + e.toString()); + // Continue though, ignoring the version number + } catch (Exception e) { + log.d(LOG_NAME, "Error: " + e.toString()); + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } + // Unable to establish version number; assume an old version by setting it to version 1 + version = 10000; + } + + private JSONObject buildRequest(String sendMethod, JSONArray params) throws JSONException { + + // Build request for method + JSONObject request = new JSONObject(); + request.put(RPC_METHOD, sendMethod); + request.put(RPC_PARAMS, (params == null) ? new JSONArray() : params); + request.put(RPC_ID, 2); + return request; + + } + + private synchronized JSONObject makeRequest(JSONObject data, Log log) throws DaemonException { + + try { + + // Initialise the HTTP client + if (httpclient == null) { + initialise(); + } + + // Login first? + if (sessionCookie == null) { + + // Build login object + String extraPass = settings.getExtraPassword(); + if (extraPass == null) { + extraPass = ""; + } + JSONObject loginRequest = new JSONObject(); + loginRequest.put(RPC_METHOD, RPC_METHOD_LOGIN); + loginRequest.put(RPC_PARAMS, (new JSONArray()).put(extraPass)); + loginRequest.put(RPC_ID, 1); + + // Set POST URL and data + HttpPost httppost = new HttpPost(buildWebUIUrl() + PATH_TO_RPC); + httppost.setHeader("content-type", "application/json"); + StringEntity se = new StringEntity(loginRequest.toString()); + httppost.setEntity(se); + + // Execute + HttpResponse response = httpclient.execute(httppost); + InputStream instream = response.getEntity().getContent(); + + // Retrieve session ID + if (!httpclient.getCookieStore().getCookies().isEmpty()) { + for (Cookie cookie : httpclient.getCookieStore().getCookies()) { + if (cookie.getName().equals(RPC_SESSION_ID)) { + sessionCookie = cookie; + break; + } + } + } + + // Still no session cookie? + if (sessionCookie == null) { + // Set error message and cancel the action that was requested + throw new DaemonException(ExceptionType.AuthenticationFailure, + "Password error? Server time difference? No (valid) cookie in response and JSON was: " + + HttpHelper.convertStreamToString(instream)); + } + + } + + // Regular action + + // Set POST URL and data + HttpPost httppost = new HttpPost(buildWebUIUrl() + PATH_TO_RPC); + httppost.setHeader("content-type", "application/json"); + StringEntity se = new StringEntity(data.toString(), HTTP.UTF_8); + httppost.setEntity(se); + + // Set session cookie, if it was not in the httpclient object yet + boolean cookiePresent = false; + for (Cookie cookie : httpclient.getCookieStore().getCookies()) { + if (cookie.getName().equals(RPC_SESSION_ID)) { + cookiePresent = true; + break; + } + } + if (!cookiePresent) { + httpclient.getCookieStore().addCookie(sessionCookie); + } + + // Execute + HttpResponse response = httpclient.execute(httppost); + + HttpEntity entity = response.getEntity(); + if (entity != null) { + + // Read JSON response + InputStream instream = entity.getContent(); + String result = HttpHelper.convertStreamToString(instream); + JSONObject json = new JSONObject(result); + instream.close(); + + log.d(LOG_NAME, "Success: " + + (result.length() > 300 ? result.substring(0, 300) + "... (" + result.length() + " chars)" : + result)); + + // Return JSON object + return json; + + } + + // No result? + throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity in response object."); + + } catch (JSONException e) { + log.d(LOG_NAME, "Error: " + e.toString()); + throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString()); + } catch (Exception e) { + log.d(LOG_NAME, "Error: " + e.toString()); + throw new DaemonException(ExceptionType.ConnectionError, e.toString()); + } + + } + + /** + * Instantiates an HTTP client with proper credentials that can be used for all Transmission requests. + * @throws DaemonException On missing settings + */ + private void initialise() throws DaemonException { + + httpclient = HttpHelper.createStandardHttpClient(settings, + settings.getUsername() != null && !settings.getUsername().equals("")); + httpclient.addRequestInterceptor(HttpHelper.gzipRequestInterceptor); + httpclient.addResponseInterceptor(HttpHelper.gzipResponseInterceptor); + + } + + /** + * Build the URL of the Transmission web UI from the user settings. + * @return The URL of the RPC API + */ + private String buildWebUIUrl() { + return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + + (settings.getFolder() == null ? "" : settings.getFolder()); + } + + private ArrayList parseJsonRetrieveTorrents(JSONObject response) throws JSONException, DaemonException { + + // Parse response + ArrayList torrents = new ArrayList(); + if (response.isNull(RPC_TORRENTS)) { + throw new DaemonException(ExceptionType.NotConnected, + "Web interface probably not connected to a daemon yet, because 'torrents' is null: " + + response.toString()); + } + JSONObject objects = response.getJSONObject(RPC_TORRENTS); + JSONArray names = objects.names(); + if (names != null) { + for (int j = 0; j < names.length(); j++) { + + JSONObject tor = objects.getJSONObject(names.getString(j)); + // Add the parsed torrent to the list + TorrentStatus status = convertDelugeState(tor.getString(RPC_STATUS)); + String error = tor.getString(RPC_MESSAGE); + if (tor.getString(RPC_TRACKER_STATUS).indexOf("Error") > 0) { + error += (error.length() > 0 ? "\n" : "") + tor.getString(RPC_TRACKER_STATUS); + //status = TorrentStatus.Error; // Don't report this as blocking error + } + // @formatter:off + torrents.add(new Torrent(j, + names.getString(j), + tor.getString(RPC_NAME), + status, + tor.getString(RPC_SAVEPATH) + settings.getOS().getPathSeperator(), + tor.getInt(RPC_RATEDOWNLOAD), + tor.getInt(RPC_RATEUPLOAD), + tor.getInt(RPC_NUMSEEDS), + tor.getInt(RPC_TOTALSEEDS), + tor.getInt(RPC_NUMPEERS), + tor.getInt(RPC_TOTALPEERS), + tor.getInt(RPC_ETA), + tor.getLong(RPC_DOWNLOADEDEVER), + tor.getLong(RPC_UPLOADEDEVER), + tor.getLong(RPC_TOTALSIZE), + ((float) tor.getDouble(RPC_PARTDONE)) / 100f, // Percentage to [0..1] + 0f, // Not available + tor.has(RPC_LABEL)? tor.getString(RPC_LABEL): null, + tor.has(RPC_TIMEADDED)? new Date((long) (tor.getDouble(RPC_TIMEADDED) * 1000L)): null, + null, // Not available + error, + settings.getType())); + // @formatter:on + } + } + + // Return the list + return torrents; + + } + + private ArrayList