Manage your torrents from your Android device
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

490 lines
17 KiB

/*
* This file is part of Transdroid <http://www.transdroid.org>
*
* Transdroid is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Transdroid is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Transdroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.transdroid.daemon.Ktorrent;
import com.android.internal.http.multipart.FilePart;
import com.android.internal.http.multipart.MultipartEntity;
import com.android.internal.http.multipart.Part;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ProtocolException;
import org.apache.http.client.RedirectHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
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.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentFile;
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.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.util.HttpHelper;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
/**
* An adapter that allows for easy access to Ktorrent's web interface. Communication is handled via HTTP GET requests
* and XML responses.
* @author erickok
*/
public class KtorrentAdapter implements IDaemonAdapter {
private static final String LOG_NAME = "Ktorrent daemon";
private static final String RPC_URL_CHALLENGE = "/login/challenge.xml";
private static final String RPC_URL_LOGIN = "/login?page=interface.html";
private static final String RPC_URL_LOGIN_USER = "username";
private static final String RPC_URL_LOGIN_PASS = "password";
private static final String RPC_URL_LOGIN_CHAL = "challenge";
private static final String RPC_URL_STATS = "/data/torrents.xml?L10n=no";
private static final String RPC_URL_ACTION = "/action?";
private static final String RPC_URL_UPLOAD = "/torrent/load?page=interface.html";
private static final String RPC_URL_FILES = "/data/torrent/files.xml?torrent=";
//private static final String RPC_COOKIE_NAME = "KT_SESSID";
private static final String RPC_SUCCESS = "<result>OK</result>";
static private int retries = 0;
private DaemonSettings settings;
private DefaultHttpClient httpclient;
/**
* Initialises an adapter that provides operations to the Ktorrent web interface
*/
public KtorrentAdapter(DaemonSettings settings) {
this.settings = settings;
}
/**
* Calculate the SHA1 hash of a password/challenge string to use with the login requests.
* @param passkey A concatenation of the challenge string and plain text password
* @return A hex-formatted SHA1-hashed string of the challenge and password strings
*/
public static String sha1Pass(String passkey) {
try {
MessageDigest m = MessageDigest.getInstance("SHA1");
byte[] data = passkey.getBytes();
m.update(data, 0, data.length);
BigInteger i = new BigInteger(1, m.digest());
return String.format("%1$040X", i).toLowerCase();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
@Override
public DaemonTaskResult executeTask(Log log, DaemonTask task) {
try {
switch (task.getMethod()) {
case Retrieve:
// Request all torrents from server
return new RetrieveTaskSuccessResult((RetrieveTask) task, makeStatsRequest(log), null);
case GetFileList:
// Request file listing for a torrent
return new GetFileListTaskSuccessResult((GetFileListTask) task,
makeFileListRequest(log, task.getTargetTorrent()));
case AddByFile:
// Add a torrent to the server by sending the contents of a local .torrent file
String file = ((AddByFileTask) task).getFile();
makeFileUploadRequest(log, file);
return null;
case AddByUrl:
// Request to add a torrent by URL
String url = ((AddByUrlTask) task).getUrl();
makeActionRequest(log, "load_torrent=" + url);
return new DaemonTaskSuccessResult(task);
case AddByMagnetUrl:
// Request to add a magnet link by URL
String magnet = ((AddByMagnetUrlTask) task).getUrl();
makeActionRequest(log, "load_torrent=" + magnet);
return new DaemonTaskSuccessResult(task);
case Remove:
// Remove a torrent
// Note that removing with data is not supported
makeActionRequest(log, "remove=" + task.getTargetTorrent().getUniqueID());
return new DaemonTaskSuccessResult(task);
case Pause:
// Pause a torrent
makeActionRequest(log, "stop=" + task.getTargetTorrent().getUniqueID());
return new DaemonTaskSuccessResult(task);
case PauseAll:
// Pause all torrents
makeActionRequest(log, "stopall=true");
return new DaemonTaskSuccessResult(task);
case Resume:
// Resume a torrent
makeActionRequest(log, "start=" + task.getTargetTorrent().getUniqueID());
return new DaemonTaskSuccessResult(task);
case ResumeAll:
// Resume all torrents
makeActionRequest(log, "startall=true");
return new DaemonTaskSuccessResult(task);
case SetFilePriorities:
// Set the priorities of the files of some torrent
SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
String act = "file_np=" + task.getTargetTorrent().getUniqueID() + "-";
switch (prioTask.getNewPriority()) {
case Off:
act = "file_stop=" + task.getTargetTorrent().getUniqueID() + "-";
break;
case Low:
case Normal:
act = "file_lp=" + task.getTargetTorrent().getUniqueID() + "-";
break;
case High:
act = "file_hp=" + task.getTargetTorrent().getUniqueID() + "-";
break;
}
// It seems KTorrent's web UI does not allow for setting all priorities in one request :(
for (TorrentFile forFile : prioTask.getForFiles()) {
makeActionRequest(log, act + forFile.getKey());
}
return new DaemonTaskSuccessResult(task);
case SetTransferRates:
// Request to set the maximum transfer rates
// TODO: Implement this?
return null;
default:
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported,
task.getMethod() + " is not supported by " + getType()));
}
} catch (LoggedOutException e) {
// Invalidate our session
httpclient = null;
if (retries < 2) {
retries++;
// Retry
log.d(LOG_NAME, "We were logged out without knowing: retry");
return executeTask(log, task);
} else {
// Never retry more than twice; in this case just return a task failure
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ConnectionError,
"Retried " + retries + " already, so we stopped now"));
}
} catch (DaemonException e) {
// Invalidate our session
httpclient = null;
// Return the task failure
return new DaemonTaskFailureResult(task, e);
}
}
private List<Torrent> makeStatsRequest(Log log) throws DaemonException, LoggedOutException {
try {
// Initialise the HTTP client
initialise();
makeLoginRequest(log);
// Make request
HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_STATS);
HttpResponse response = httpclient.execute(httpget);
// Read XML response
InputStream instream = response.getEntity().getContent();
List<Torrent> torrents = StatsParser.parse(new InputStreamReader(instream), settings.getDownloadDir(),
settings.getOS().getPathSeperator());
instream.close();
return torrents;
} catch (LoggedOutException e) {
throw e;
} catch (DaemonException e) {
log.d(LOG_NAME, "Parsing error: " + e.toString());
throw e;
} catch (Exception e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
}
}
private List<TorrentFile> makeFileListRequest(Log log, Torrent torrent) throws DaemonException, LoggedOutException {
try {
// Initialise the HTTP client
initialise();
makeLoginRequest(log);
// Make request
HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_FILES + torrent.getUniqueID());
HttpResponse response = httpclient.execute(httpget);
// Read XML response
InputStream instream = response.getEntity().getContent();
List<TorrentFile> files = FileListParser.parse(new InputStreamReader(instream), torrent.getLocationDir());
instream.close();
// If the files list is empty, it means that this is a single-file torrent
// We can mimic this single file form the torrent statistics itself
files.add(new TorrentFile("" + 0, torrent.getName(), torrent.getName(),
torrent.getLocationDir() + torrent.getName(), torrent.getTotalSize(), torrent.getDownloadedEver(),
Priority.Normal));
return files;
} catch (LoggedOutException e) {
throw e;
} catch (DaemonException e) {
log.d(LOG_NAME, "Parsing error: " + e.toString());
throw e;
} catch (Exception e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
}
}
private void makeLoginRequest(Log log) throws DaemonException {
try {
// Make challenge request
HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_CHALLENGE);
HttpResponse response = httpclient.execute(httpget);
InputStream instream = response.getEntity().getContent();
String challengeString = HttpHelper.convertStreamToString(instream).replaceAll("<.*?>", "").trim();
instream.close();
// Challenge string should be something like TncpX3TB8uZ0h8eqztZ6
if (challengeString.length() != 20) {
throw new DaemonException(ExceptionType.UnexpectedResponse, "No (valid) challenge string received");
}
// Make login request
HttpPost httppost2 = new HttpPost(buildWebUIUrl() + RPC_URL_LOGIN);
List<NameValuePair> params = new ArrayList<NameValuePair>(3);
params.add(new BasicNameValuePair(RPC_URL_LOGIN_USER, settings.getUsername()));
params.add(new BasicNameValuePair(RPC_URL_LOGIN_PASS,
"")); // Password is send (as SHA1 hex) in the challenge field
params.add(new BasicNameValuePair(RPC_URL_LOGIN_CHAL, sha1Pass(challengeString +
settings.getPassword()))); // Make a SHA1 encrypted hex-formated string of the challenge code and password
httppost2.setEntity(new UrlEncodedFormEntity(params));
// This sets the authentication cookie
httpclient.execute(httppost2);
/*InputStream instream2 = response2.getEntity().getContent();
String result2 = HttpHelper.ConvertStreamToString(instream2);
instream2.close();*/
// Successfully logged in; we may retry later if needed
retries = 0;
} catch (DaemonException e) {
log.d(LOG_NAME, "Login error: " + e.toString());
throw e;
} catch (Exception e) {
log.d(LOG_NAME, "Error during login: " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
}
}
private boolean makeActionRequest(Log log, String action) throws DaemonException, LoggedOutException {
try {
// Initialise the HTTP client
initialise();
makeLoginRequest(log);
// Make request
HttpGet httpget = new HttpGet(buildWebUIUrl() + RPC_URL_ACTION + action);
HttpResponse response = httpclient.execute(httpget);
// Read response (a successful action always returned '1')
InputStream instream = response.getEntity().getContent();
String result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.contains(RPC_SUCCESS)) {
return true;
} else if (result.contains("KTorrent WebInterface - Login")) {
// Apparently we were returned an HTML page instead of the expected XML
// This happens in particular when we were logged out (because somebody else logged into KTorrent's web interface)
throw new LoggedOutException();
} else {
throw new DaemonException(ExceptionType.UnexpectedResponse, "Action response was not OK but " + result);
}
} catch (LoggedOutException e) {
throw e;
} catch (DaemonException e) {
log.d(LOG_NAME, action + " request error: " + e.toString());
throw e;
} catch (Exception e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
}
}
private boolean makeFileUploadRequest(Log log, String target) throws DaemonException, LoggedOutException {
try {
// Initialise the HTTP client
initialise();
makeLoginRequest(log);
// Make request
HttpPost httppost = new HttpPost(buildWebUIUrl() + RPC_URL_UPLOAD);
File upload = new File(URI.create(target));
Part[] parts = {new FilePart("load_torrent", upload)};
httppost.setEntity(new MultipartEntity(parts, httppost.getParams()));
// Make sure we are not automatically redirected
RedirectHandler handler = new RedirectHandler() {
@Override
public boolean isRedirectRequested(HttpResponse response, HttpContext context) {
return false;
}
@Override
public URI getLocationURI(HttpResponse response, HttpContext context) throws ProtocolException {
return null;
}
};
httpclient.setRedirectHandler(handler);
HttpResponse response = httpclient.execute(httppost);
// Read response (a successful action always returned '1')
InputStream instream = response.getEntity().getContent();
String result = HttpHelper.convertStreamToString(instream);
instream.close();
if (result.equals("")) {
return true;
} else if (result.contains("KTorrent WebInterface - Login")) {
// Apparently we were returned an HTML page instead of the expected XML
// This happens in particular when we were logged out (because somebody else logged into KTorrent's web interface)
throw new LoggedOutException();
} else {
throw new DaemonException(ExceptionType.UnexpectedResponse, "Action response was not 1 but " + result);
}
} catch (LoggedOutException e) {
throw e;
} catch (DaemonException e) {
log.d(LOG_NAME, "File upload error: " + e.toString());
throw e;
} catch (Exception e) {
log.d(LOG_NAME, "Error: " + e.toString());
throw new DaemonException(ExceptionType.ConnectionError, e.toString());
}
}
/**
* Indicates if we were already successfully authenticated
* @return True if the proper authentication cookie was already loaded
*/
/*private boolean authenticated() {
// We should have a Ktorrent cookie in the httpclient when we are authenticated
for(Cookie cookie : httpclient.getCookieStore().getCookies()) {
if (cookie.getName().equals(RPC_COOKIE_NAME)) {
return true;
}
}
return false;
}*/
/**
* Instantiates an HTTP client that can be used for all Ktorrent requests.
* @throws DaemonException Thrown on settings error
*/
private void initialise() throws DaemonException {
if (httpclient != null) {
httpclient = null;
}
httpclient = HttpHelper.createStandardHttpClient(settings, false);
}
/**
* Build the base URL for a Ktorrent web site request from the user settings.
* @return The base URL of for a request, i.e. http://localhost:8080
*/
private String buildWebUIUrl() {
return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort();
}
@Override
public Daemon getType() {
return settings.getType();
}
@Override
public DaemonSettings getSettings() {
return this.settings;
}
}