diff --git a/.classpath b/.classpath index fadbb9f4b..b6c82120d 100644 --- a/.classpath +++ b/.classpath @@ -4,7 +4,11 @@ - + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea7c7e0d..3943d2260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/org/fdroid/fdroid/FDroidApp.java b/src/org/fdroid/fdroid/FDroidApp.java index e5d8c13fe..16d936fce 100644 --- a/src/org/fdroid/fdroid/FDroidApp.java +++ b/src/org/fdroid/fdroid/FDroidApp.java @@ -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() { diff --git a/src/org/fdroid/fdroid/localrepo/LocalRepoService.java b/src/org/fdroid/fdroid/localrepo/LocalRepoService.java index fc182352d..d05826b83 100644 --- a/src/org/fdroid/fdroid/localrepo/LocalRepoService.java +++ b/src/org/fdroid/fdroid/localrepo/LocalRepoService.java @@ -216,25 +216,31 @@ public class LocalRepoService extends Service { } private void registerMDNSService() { - String repoName = Preferences.get().getLocalRepoName(); - final HashMap values = new HashMap(); - 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 values = new HashMap(); + 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); diff --git a/src/org/fdroid/fdroid/net/ApkDownloader.java b/src/org/fdroid/fdroid/net/ApkDownloader.java index baddfd5ef..c8c6bcc15 100644 --- a/src/org/fdroid/fdroid/net/ApkDownloader.java +++ b/src/org/fdroid/fdroid/net/ApkDownloader.java @@ -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; diff --git a/src/org/fdroid/fdroid/net/Downloader.java b/src/org/fdroid/fdroid/net/Downloader.java index 8c9f7cf77..593777d52 100644 --- a/src/org/fdroid/fdroid/net/Downloader.java +++ b/src/org/fdroid/fdroid/net/Downloader.java @@ -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); diff --git a/src/org/fdroid/fdroid/net/DownloaderFactory.java b/src/org/fdroid/fdroid/net/DownloaderFactory.java new file mode 100644 index 000000000..df0ab415b --- /dev/null +++ b/src/org/fdroid/fdroid/net/DownloaderFactory.java @@ -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/.*"); + } +} diff --git a/src/org/fdroid/fdroid/net/HttpDownloader.java b/src/org/fdroid/fdroid/net/HttpDownloader.java index 177fa6de1..f1dab1ffc 100644 --- a/src/org/fdroid/fdroid/net/HttpDownloader.java +++ b/src/org/fdroid/fdroid/net/HttpDownloader.java @@ -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; } diff --git a/src/org/fdroid/fdroid/net/IconDownloader.java b/src/org/fdroid/fdroid/net/IconDownloader.java new file mode 100644 index 000000000..a8dedf17a --- /dev/null +++ b/src/org/fdroid/fdroid/net/IconDownloader.java @@ -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); + } + } +} diff --git a/src/org/fdroid/fdroid/net/TorHttpDownloader.java b/src/org/fdroid/fdroid/net/TorHttpDownloader.java new file mode 100644 index 000000000..824ff6433 --- /dev/null +++ b/src/org/fdroid/fdroid/net/TorHttpDownloader.java @@ -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); + } +} diff --git a/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/src/org/fdroid/fdroid/net/WifiStateChangeService.java index c24271ff9..6b4fda481 100644 --- a/src/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/src/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -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 diff --git a/src/org/fdroid/fdroid/updater/RepoUpdater.java b/src/org/fdroid/fdroid/updater/RepoUpdater.java index eab4291d7..7cb788ed3 100644 --- a/src/org/fdroid/fdroid/updater/RepoUpdater.java +++ b/src/org/fdroid/fdroid/updater/RepoUpdater.java @@ -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