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
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; |
|
} |
|
|
|
}
|
|
|