Merge branch 'jmdns-fixes-and-tor-onion-support' into 'master'

Jmdns fixes and tor onion support

There are three groups of work in this collection of commits:

* improvements to the `WifiStateChangeService` and related activities like JmDNS to eliminate problems that happen when there are a lot of wifi change events.

* add rework the `.net.Downloader` stuff to add Tor support and lay the groundwork for Bluetooth support

* add support for repos on Tor Hidden Service .onion addresses
This commit is contained in:
Peter Serwylo 2014-05-31 02:25:19 +00:00
commit 95180512c7
12 changed files with 218 additions and 76 deletions

View File

@ -4,7 +4,11 @@
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="extern/symlinks-for-ant-and-eclipse"/>
<classpathentry kind="src" path="extern/symlinks-for-ant-and-eclipse">
<attributes>
<attribute name="ignore_optional_problems" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="gen"/>
<classpathentry combineaccessrules="false" kind="src" path="/AndroidPinning"/>
<classpathentry combineaccessrules="false" kind="src" path="/MemorizingActivity"/>

View File

@ -8,6 +8,8 @@
* find local repos on the same network using Bonjour/mDNS
* use FDroid repos on Tor Hidden Services (.onion addresses)
* directly send installed apps to other devices via Bluetooth and Android Beam
(NFC+Bluetooth), also compatible with Samsung/HTC S-Beam

View File

@ -41,11 +41,13 @@ import com.nostra13.universalimageloader.utils.StorageUtils;
import de.duenndns.ssl.MemorizingTrustManager;
import org.fdroid.fdroid.Preferences.ChangeListener;
import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.net.IconDownloader;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.thoughtcrime.ssl.pinning.PinningTrustManager;
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
@ -151,7 +153,8 @@ public class FDroidApp extends Application {
bluetoothAdapter = getBluetoothAdapter();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext())
.discCache(new LimitedAgeDiscCache(
.imageDownloader(new IconDownloader(getApplicationContext()))
.diskCache(new LimitedAgeDiscCache(
new File(StorageUtils.getCacheDirectory(getApplicationContext(), true),
"icons"),
null,
@ -213,6 +216,13 @@ public class FDroidApp extends Application {
if (wifiState == WifiManager.WIFI_STATE_ENABLING
|| wifiState == WifiManager.WIFI_STATE_ENABLED)
startService(new Intent(this, WifiStateChangeService.class));
// if the HTTPS pref changes, then update all affected things
Preferences.get().registerLocalRepoHttpsListeners(new ChangeListener() {
@Override
public void onPreferenceChange() {
startService(new Intent(FDroidApp.this, WifiStateChangeService.class));
}
});
}
@TargetApi(18)
@ -282,17 +292,21 @@ public class FDroidApp extends Application {
public static void startLocalRepoService(Context context) {
if (!localRepoServiceIsBound) {
Context app = context.getApplicationContext();
app.bindService(new Intent(app, LocalRepoService.class),
serviceConnection, Context.BIND_AUTO_CREATE);
localRepoServiceIsBound = true;
Intent service = new Intent(app, LocalRepoService.class);
localRepoServiceIsBound = app.bindService(service, serviceConnection,
Context.BIND_AUTO_CREATE);
if (localRepoServiceIsBound)
app.startService(service);
}
}
public static void stopLocalRepoService(Context context) {
Context app = context.getApplicationContext();
if (localRepoServiceIsBound) {
context.getApplicationContext().unbindService(serviceConnection);
app.unbindService(serviceConnection);
localRepoServiceIsBound = false;
}
app.stopService(new Intent(app, LocalRepoService.class));
}
public static void restartLocalRepoService() {

View File

@ -216,25 +216,31 @@ public class LocalRepoService extends Service {
}
private void registerMDNSService() {
String repoName = Preferences.get().getLocalRepoName();
final HashMap<String, String> values = new HashMap<String, String>();
values.put("path", "/fdroid/repo");
values.put("name", repoName);
values.put("fingerprint", FDroidApp.repo.fingerprint);
String type;
if (Preferences.get().isLocalRepoHttpsEnabled()) {
values.put("type", "fdroidrepos");
type = "_https._tcp.local.";
} else {
values.put("type", "fdroidrepo");
type = "_http._tcp.local.";
}
pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
new Thread(new Runnable() {
@Override
public void run() {
/*
* a ServiceInfo can only be registered with a single instance
* of JmDNS, and there is only ever a single LocalHTTPD port to
* advertise anyway.
*/
if (pairService != null || jmdns != null)
clearCurrentMDNSService();
String repoName = Preferences.get().getLocalRepoName();
HashMap<String, String> values = new HashMap<String, String>();
values.put("path", "/fdroid/repo");
values.put("name", repoName);
values.put("fingerprint", FDroidApp.repo.fingerprint);
String type;
if (Preferences.get().isLocalRepoHttpsEnabled()) {
values.put("type", "fdroidrepos");
type = "_https._tcp.local.";
} else {
values.put("type", "fdroidrepo");
type = "_http._tcp.local.";
}
try {
pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
jmdns = JmDNS.create();
jmdns.registerService(pairService);
} catch (IOException e) {
@ -249,6 +255,10 @@ public class LocalRepoService extends Service {
Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
localRepoBonjourChangeListener = null;
}
clearCurrentMDNSService();
}
private void clearCurrentMDNSService() {
if (jmdns != null) {
if (pairService != null) {
jmdns.unregisterService(pairService);

View File

@ -22,6 +22,7 @@ package org.fdroid.fdroid.net;
import android.os.Bundle;
import android.util.Log;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.data.Apk;
@ -172,8 +173,7 @@ public class ApkDownloader implements AsyncDownloadWrapper.Listener {
Log.d(TAG, "Downloading apk from " + remoteAddress);
try {
Downloader downloader = new HttpDownloader(remoteAddress, localFile);
Downloader downloader = DownloaderFactory.create(remoteAddress, localFile);
dlWrapper = new AsyncDownloadWrapper(downloader, this);
dlWrapper.download();
return true;

View File

@ -14,6 +14,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
public abstract class Downloader {
@ -23,31 +24,32 @@ public abstract class Downloader {
private ProgressListener progressListener = null;
private Bundle eventData = null;
private File outputFile;
protected URL sourceUrl;
protected String cacheTag = null;
public static final String EVENT_PROGRESS = "downloadProgress";
public abstract InputStream inputStream() throws IOException;
public abstract InputStream getInputStream() throws IOException;
// The context is required for opening the file to write to.
public Downloader(String destFile, Context ctx)
Downloader(String destFile, Context ctx)
throws FileNotFoundException, MalformedURLException {
this(new File(ctx.getFilesDir() + File.separator + destFile));
}
// The context is required for opening the file to write to.
public Downloader(Context ctx) throws IOException {
Downloader(Context ctx) throws IOException {
this(File.createTempFile("dl-", "", ctx.getCacheDir()));
}
public Downloader(File destFile)
Downloader(File destFile)
throws FileNotFoundException, MalformedURLException {
// http://developer.android.com/guide/topics/data/data-storage.html#InternalCache
outputFile = destFile;
outputStream = new FileOutputStream(outputFile);
}
public Downloader(OutputStream output)
Downloader(OutputStream output)
throws MalformedURLException {
outputStream = output;
outputFile = null;
@ -118,13 +120,13 @@ public abstract class Downloader {
Log.d(TAG, "Downloading from stream");
InputStream input = null;
try {
input = inputStream();
input = getInputStream();
// Getting the input stream is slow(ish) for HTTP downloads, so we'll check if
// we were interrupted before proceeding to the download.
throwExceptionIfInterrupted();
copyInputToOutputStream(inputStream());
copyInputToOutputStream(getInputStream());
} finally {
Utils.closeQuietly(outputStream);
Utils.closeQuietly(input);

View File

@ -0,0 +1,32 @@
package org.fdroid.fdroid.net;
import android.content.Context;
import java.io.File;
import java.io.IOException;
public class DownloaderFactory {
public static Downloader create(String url, Context context)
throws IOException {
if (isOnionAddress(url)) {
return new TorHttpDownloader(url, context);
} else {
return new HttpDownloader(url, context);
}
}
public static Downloader create(String url, File destFile)
throws IOException {
if (isOnionAddress(url)) {
return new TorHttpDownloader(url, destFile);
} else {
return new HttpDownloader(url, destFile);
}
}
private static boolean isOnionAddress(String url) {
return url.matches("^[a-zA-Z0-9]+://[^/]+\\.onion/.*");
}
}

View File

@ -7,33 +7,23 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import javax.net.ssl.SSLHandshakeException;
public class HttpDownloader extends Downloader {
private static final String TAG = "org.fdroid.fdroid.net.HttpDownloader";
private static final String HEADER_IF_NONE_MATCH = "If-None-Match";
private static final String HEADER_FIELD_ETAG = "ETag";
protected static final String HEADER_IF_NONE_MATCH = "If-None-Match";
protected static final String HEADER_FIELD_ETAG = "ETag";
private URL sourceUrl;
private HttpURLConnection connection;
protected HttpURLConnection connection;
private int statusCode = -1;
// The context is required for opening the file to write to.
public HttpDownloader(String source, String destFile, Context ctx)
throws FileNotFoundException, MalformedURLException {
super(destFile, ctx);
sourceUrl = new URL(source);
}
// The context is required for opening the file to write to.
public HttpDownloader(String source, File destFile)
HttpDownloader(String source, File destFile)
throws FileNotFoundException, MalformedURLException {
super(destFile);
sourceUrl = new URL(source);
@ -42,20 +32,17 @@ public class HttpDownloader extends Downloader {
/**
* Downloads to a temporary file, which *you must delete yourself when
* you are done*.
* @see org.fdroid.fdroid.net.HttpDownloader#getFile()
* @see org.fdroid.fdroid.net.Downloader#getFile()
*/
public HttpDownloader(String source, Context ctx) throws IOException {
HttpDownloader(String source, Context ctx) throws IOException {
super(ctx);
sourceUrl = new URL(source);
}
public HttpDownloader(String source, OutputStream output)
throws MalformedURLException {
super(output);
sourceUrl = new URL(source);
}
public InputStream inputStream() throws IOException {
@Override
public InputStream getInputStream() throws IOException {
setupConnection();
// TODO check out BaseImageDownloader.getStreamFromNetwork() for optims
return connection.getInputStream();
}
@ -68,31 +55,41 @@ public class HttpDownloader extends Downloader {
@Override
public void download() throws IOException, InterruptedException {
try {
connection = (HttpURLConnection)sourceUrl.openConnection();
if (wantToCheckCache()) {
setupCacheCheck();
Log.i(TAG, "Checking cached status of " + sourceUrl);
statusCode = connection.getResponseCode();
}
if (isCached()) {
Log.i(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")");
} else {
Log.i(TAG, "Downloading from " + sourceUrl);
downloadFromStream();
updateCacheCheck();
}
setupConnection();
doDownload();
} catch (SSLHandshakeException e) {
// TODO this should be handled better, it is not internationalised here.
// TODO this should be handled better, it is not internationalised here
throw new IOException(
"A problem occurred while establishing an SSL " +
"connection. If this problem persists, AND you have a " +
"very old device, you could try using http instead of " +
"https for the repo URL." + Log.getStackTraceString(e) );
"https for the repo URL." + Log.getStackTraceString(e));
}
}
protected void setupConnection() throws IOException {
if (connection != null)
return;
connection = (HttpURLConnection) sourceUrl.openConnection();
}
protected void doDownload() throws IOException, InterruptedException {
if (wantToCheckCache()) {
setupCacheCheck();
Log.i(TAG, "Checking cached status of " + sourceUrl);
statusCode = connection.getResponseCode();
}
if (isCached()) {
Log.i(TAG, sourceUrl + " is cached, so not downloading (HTTP " + statusCode + ")");
} else {
Log.i(TAG, "Downloading from " + sourceUrl);
downloadFromStream();
updateCacheCheck();
}
}
@Override
public boolean isCached() {
return wantToCheckCache() && statusCode == 304;
}

View File

@ -0,0 +1,32 @@
package org.fdroid.fdroid.net;
import android.content.Context;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import java.io.IOException;
import java.io.InputStream;
public class IconDownloader extends BaseImageDownloader {
public IconDownloader(Context context) {
super(context);
}
public IconDownloader(Context context, int connectTimeout, int readTimeout) {
super(context, connectTimeout, readTimeout);
}
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
switch (Scheme.ofUri(imageUri)) {
case HTTP:
case HTTPS:
Downloader downloader = DownloaderFactory.create(imageUri, context);
return downloader.getInputStream();
default:
return super.getStream(imageUri, extra);
}
}
}

View File

@ -0,0 +1,32 @@
package org.fdroid.fdroid.net;
import android.content.Context;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.SocketAddress;
public class TorHttpDownloader extends HttpDownloader {
TorHttpDownloader(String url, Context ctx) throws IOException {
super(url, ctx);
}
TorHttpDownloader(String url, File destFile)
throws FileNotFoundException, MalformedURLException {
super(url, destFile);
}
@Override
protected void setupConnection() throws IOException {
SocketAddress sa = new InetSocketAddress("127.0.0.1", 8118);
Proxy tor = new Proxy(Proxy.Type.HTTP, sa);
connection = (HttpURLConnection) sourceUrl.openConnection(tor);
}
}

View File

@ -23,9 +23,14 @@ import java.util.Locale;
public class WifiStateChangeService extends Service {
public static final String BROADCAST = "org.fdroid.fdroid.action.WIFI_CHANGE";
private static WaitForWifiAsyncTask asyncTask;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new WaitForWifiAsyncTask().execute();
if (asyncTask != null)
asyncTask.cancel(true);
asyncTask = new WaitForWifiAsyncTask();
asyncTask.execute();
return START_NOT_STICKY;
}
@ -38,15 +43,21 @@ public class WifiStateChangeService extends Service {
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
try {
while (!wifiManager.isWifiEnabled()) {
if (isCancelled())
return null;
Log.i(TAG, "waiting for the wifi to be enabled...");
Thread.sleep(3000);
Thread.sleep(1000);
}
int ipAddress = wifiManager.getConnectionInfo().getIpAddress();
while (ipAddress == 0) {
if (isCancelled())
return null;
Log.i(TAG, "waiting for an IP address...");
Thread.sleep(3000);
Thread.sleep(1000);
ipAddress = wifiManager.getConnectionInfo().getIpAddress();
}
if (isCancelled())
return null;
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
ipAddress = wifiInfo.getIpAddress();
FDroidApp.ipAddressString = String.format(Locale.ENGLISH, "%d.%d.%d.%d",
@ -67,6 +78,9 @@ public class WifiStateChangeService extends Service {
FDroidApp.repo.address = String.format(Locale.ENGLISH, "%s://%s:%d/fdroid/repo",
scheme, FDroidApp.ipAddressString, FDroidApp.port);
if (isCancelled())
return null;
Context context = WifiStateChangeService.this.getApplicationContext();
LocalRepoKeyStore localRepoKeyStore = LocalRepoKeyStore.get(context);
Certificate localCert = localRepoKeyStore.getCertificate();
@ -75,6 +89,9 @@ public class WifiStateChangeService extends Service {
lrm.setUriString(FDroidApp.repo.address);
lrm.writeIndexPage(Utils.getSharingUri(context, FDroidApp.repo).toString());
if (isCancelled())
return null;
/*
* Once the IP address is known we need to generate a self
* signed certificate to use for HTTPS that has a CN field set

View File

@ -13,7 +13,7 @@ import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.HttpDownloader;
import org.fdroid.fdroid.net.DownloaderFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
@ -89,7 +89,7 @@ abstract public class RepoUpdater {
protected Downloader downloadIndex() throws UpdateException {
Downloader downloader = null;
try {
downloader = new HttpDownloader(getIndexAddress(), context);
downloader = DownloaderFactory.create(getIndexAddress(), context);
downloader.setCacheTag(repo.lastetag);
if (progressListener != null) { // interactive session, show progress