Alon Albert
7 years ago
7 changed files with 356 additions and 213 deletions
@ -0,0 +1,169 @@ |
|||||||
|
package org.transdroid.daemon.Deluge; |
||||||
|
|
||||||
|
import android.support.annotation.NonNull; |
||||||
|
import android.text.TextUtils; |
||||||
|
import deluge.impl.net.AcceptAllTrustManager; |
||||||
|
import java.io.ByteArrayOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.net.Socket; |
||||||
|
import java.net.UnknownHostException; |
||||||
|
import java.security.KeyManagementException; |
||||||
|
import java.security.NoSuchAlgorithmException; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.SortedMap; |
||||||
|
import java.util.TreeMap; |
||||||
|
import java.util.zip.DeflaterOutputStream; |
||||||
|
import java.util.zip.InflaterInputStream; |
||||||
|
import javax.net.ssl.SSLContext; |
||||||
|
import javax.net.ssl.TrustManager; |
||||||
|
import org.transdroid.daemon.DaemonException; |
||||||
|
import org.transdroid.daemon.DaemonException.ExceptionType; |
||||||
|
import se.dimovski.rencode.Rencode; |
||||||
|
|
||||||
|
/** |
||||||
|
* A Deluge RPC API Client. |
||||||
|
*/ |
||||||
|
public class DelugeRpcClient { |
||||||
|
// TODO: Extract constants to a common file used by both Adapters.
|
||||||
|
private static final String RPC_METHOD_LOGIN = "daemon.login"; |
||||||
|
private static final int RPC_ERROR = 2; |
||||||
|
|
||||||
|
private final String address; |
||||||
|
private final int port; |
||||||
|
private final String username; |
||||||
|
private final String password; |
||||||
|
|
||||||
|
public DelugeRpcClient(String address, int port, String username, String password) { |
||||||
|
this.address = address; |
||||||
|
this.port = port; |
||||||
|
this.username = username; |
||||||
|
this.password = password; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
Object sendRequest(String method, Object... args) throws DaemonException { |
||||||
|
final List<Object> results = sendRequests(new Request(method, args)); |
||||||
|
return results.get(0); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
private List<Object> sendRequests(Request... requests) throws DaemonException { |
||||||
|
final List<Object> requestObjects = new ArrayList<>(); |
||||||
|
|
||||||
|
int loginRequestId = -1; |
||||||
|
if (!TextUtils.isEmpty(username)) { |
||||||
|
final Request loginRequest = new Request(RPC_METHOD_LOGIN, username, password); |
||||||
|
requestObjects.add(loginRequest.toObject()); |
||||||
|
loginRequestId = loginRequest.getId(); |
||||||
|
} |
||||||
|
for (Request request : requests) { |
||||||
|
requestObjects.add(request.toObject()); |
||||||
|
} |
||||||
|
final byte[] requestBytes; |
||||||
|
try { |
||||||
|
requestBytes = compress(Rencode.encode(requestObjects)); |
||||||
|
} catch (IOException e) { |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, |
||||||
|
"Failed to encode request: " + e.getMessage()); |
||||||
|
} |
||||||
|
|
||||||
|
final Socket socket = openSocket(); |
||||||
|
try { |
||||||
|
socket.getOutputStream().write(requestBytes); |
||||||
|
final SortedMap<Integer, Object> returnValuesMap = new TreeMap<>(); |
||||||
|
for (int i = 0, n = requestObjects.size(); i < n; i++) { |
||||||
|
final Response response = readResponse(socket.getInputStream()); |
||||||
|
final int responseId = response.getId(); |
||||||
|
if (response.getType() == RPC_ERROR) { |
||||||
|
if (responseId == loginRequestId) { |
||||||
|
throw new DaemonException(ExceptionType.AuthenticationFailure, response.getReturnValue() |
||||||
|
.toString()); |
||||||
|
} else { |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, response.getReturnValue().toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
returnValuesMap.put(response.getId(), response.getReturnValue()); |
||||||
|
} |
||||||
|
if (returnValuesMap.size() != requestObjects.size()) { |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, returnValuesMap.toString()); |
||||||
|
|
||||||
|
} |
||||||
|
final List<Object> returnValues = new ArrayList<>(); |
||||||
|
for (Request request : requests) { |
||||||
|
final int requestId = request.getId(); |
||||||
|
final Object returnValue = returnValuesMap.get(requestId); |
||||||
|
if (returnValue == null) { |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, "No result for request id " + requestId); |
||||||
|
} |
||||||
|
returnValues.add(returnValue); |
||||||
|
} |
||||||
|
return returnValues; |
||||||
|
} catch (IOException e) { |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, e.getMessage()); |
||||||
|
} finally { |
||||||
|
try { |
||||||
|
socket.close(); |
||||||
|
} catch (IOException e) { |
||||||
|
// ignore
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
private byte[] compress(byte[] bytes) throws IOException { |
||||||
|
ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); |
||||||
|
try { |
||||||
|
DeflaterOutputStream deltaterOut = new DeflaterOutputStream(byteOut); |
||||||
|
try { |
||||||
|
deltaterOut.write(bytes); |
||||||
|
deltaterOut.finish(); |
||||||
|
return byteOut.toByteArray(); |
||||||
|
} finally { |
||||||
|
deltaterOut.close(); |
||||||
|
} |
||||||
|
} finally { |
||||||
|
byteOut.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
private Response readResponse(InputStream in) throws DaemonException, IOException { |
||||||
|
final InflaterInputStream inflater = new InflaterInputStream(in); |
||||||
|
final ByteArrayOutputStream out = new ByteArrayOutputStream(); |
||||||
|
final byte[] buffer = new byte[1024]; |
||||||
|
while (inflater.available() > 0) { |
||||||
|
final int n = inflater.read(buffer); |
||||||
|
if (n > 0) { |
||||||
|
out.write(buffer, 0, n); |
||||||
|
} |
||||||
|
} |
||||||
|
final byte[] bytes = out.toByteArray(); |
||||||
|
return new Response(Rencode.decode(bytes)); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
private Socket openSocket() throws DaemonException { |
||||||
|
try { |
||||||
|
final TrustManager[] trustAllCerts = new TrustManager[]{new AcceptAllTrustManager()}; |
||||||
|
final SSLContext sslContext = SSLContext.getInstance("TLSv1"); |
||||||
|
sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); |
||||||
|
|
||||||
|
return sslContext.getSocketFactory().createSocket(address, port); |
||||||
|
} catch (NoSuchAlgorithmException e) { |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, |
||||||
|
"Failed to open socket: " + e.getMessage()); |
||||||
|
} catch (UnknownHostException e) { |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, |
||||||
|
"Failed to open socket: " + e.getMessage()); |
||||||
|
} catch (IOException e) { |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, |
||||||
|
"Failed to open socket: " + e.getMessage()); |
||||||
|
} catch (KeyManagementException e) { |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, |
||||||
|
"Failed to open socket: " + e.getMessage()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package org.transdroid.daemon.Deluge; |
||||||
|
|
||||||
|
/** |
||||||
|
* Used to count torrents in labels. |
||||||
|
*/ |
||||||
|
class MutableInt { |
||||||
|
|
||||||
|
int value = 1; |
||||||
|
|
||||||
|
MutableInt(int value) { |
||||||
|
this.value = value; |
||||||
|
} |
||||||
|
|
||||||
|
void increment() { |
||||||
|
++value; |
||||||
|
} |
||||||
|
|
||||||
|
int get() { |
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
package org.transdroid.daemon.Deluge; |
||||||
|
|
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||||
|
|
||||||
|
/** |
||||||
|
* A Deluge RPC Request wrapper |
||||||
|
*/ |
||||||
|
class Request { |
||||||
|
private static AtomicInteger requestIdCounter = new AtomicInteger(); |
||||||
|
|
||||||
|
private final int id; |
||||||
|
private final String method; |
||||||
|
private final Object[] args; |
||||||
|
|
||||||
|
public Request(String method, Object... args) { |
||||||
|
id = requestIdCounter.getAndIncrement(); |
||||||
|
this.method = method; |
||||||
|
this.args = args; |
||||||
|
} |
||||||
|
|
||||||
|
public Object toObject() { |
||||||
|
return new Object[] {id, method, args, new HashMap<>()}; |
||||||
|
} |
||||||
|
|
||||||
|
public int getId() { |
||||||
|
return id; |
||||||
|
} |
||||||
|
|
||||||
|
public String getMethod() { |
||||||
|
return method; |
||||||
|
} |
||||||
|
|
||||||
|
public Object[] getArgs() { |
||||||
|
return args; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
package org.transdroid.daemon.Deluge; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import org.transdroid.daemon.DaemonException; |
||||||
|
import org.transdroid.daemon.DaemonException.ExceptionType; |
||||||
|
|
||||||
|
/** |
||||||
|
* A Deluge RPC Response wrapper |
||||||
|
*/ |
||||||
|
class Response { |
||||||
|
|
||||||
|
private static final int RESPONSE_TYPE_INDEX = 0; |
||||||
|
private static final int RESPONSE_ID_INDEX = 1; |
||||||
|
private static final int RESPONSE_RETURN_VALUE_INDEX = 2; |
||||||
|
|
||||||
|
private final int type; |
||||||
|
private final int id; |
||||||
|
private final Object returnValue; |
||||||
|
|
||||||
|
public Response(Object responseObject) throws DaemonException { |
||||||
|
if (!(responseObject instanceof List)) { |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, responseObject.toString()); |
||||||
|
} |
||||||
|
final List response = (List) responseObject; |
||||||
|
|
||||||
|
if (response.size() < RESPONSE_RETURN_VALUE_INDEX + 1) { |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, responseObject.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
if (!(response.get(RESPONSE_TYPE_INDEX) instanceof Number)) { |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, responseObject.toString()); |
||||||
|
} |
||||||
|
type = ((Number) (response.get(RESPONSE_TYPE_INDEX))).intValue(); |
||||||
|
|
||||||
|
if (!(response.get(RESPONSE_ID_INDEX) instanceof Number)) { |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, responseObject.toString()); |
||||||
|
} |
||||||
|
id = ((Number) (response.get(RESPONSE_ID_INDEX))).intValue(); |
||||||
|
|
||||||
|
returnValue = response.get(RESPONSE_RETURN_VALUE_INDEX); |
||||||
|
} |
||||||
|
|
||||||
|
public int getType() { |
||||||
|
return type; |
||||||
|
} |
||||||
|
|
||||||
|
public int getId() { |
||||||
|
return id; |
||||||
|
} |
||||||
|
|
||||||
|
public Object getReturnValue() { |
||||||
|
return returnValue; |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue