3

I'm trying to load map tiles from an internal SSL server. The SSL certificate's root of trust is not recognized by the Android system.

W/o*.o*.t*.m*.MapTileDow*(2837): IOException downloading MapTile: /8/37/4 : javax.net.ssl.SSLPeerUnverifiedException: No peer certificate

I'm already familiar with the problem and have solved it in the rest of the application based on this excellent SO answer. Essentially, I extended my own SSLSocketFactory and X509TrustManager which load my SSL certificate's root of trust from a .bks file bundled with the app. To create a secure connection, I call ((HttpsURLConnection) connection).setSSLSocketFactory(mySSLSocketFactory) and the certificate is verified using my classes with my root of trust.

My question is how do I do the same thing for osmdroid? I'm creating my own XYTileSource where I set the URL, file extension, size, etc. of my map tiles. I see that osmdroid creates its connections to download map tile images in MapTileDownloader. I can write my own replacement class that will address the SSL issue in the same manner, but how do I tell osmdroid to use my custom downloader instead of the default?

Community
  • 1
  • 1
quietmint
  • 13,885
  • 6
  • 48
  • 73

2 Answers2

1

It turns out this is possible without changing the source of osmdroid, due to the public MapView(Context context, int tileSizePixels, ResourceProxy resourceProxy, MapTileProviderBase aTileProvider) constrtuctor.

Assuming you already have a custom class like MySSLSocketFactory (which extends javax.net.ssl.SSLSocketFactory), the basic process looks like this:

  1. Create a drop-in replacement class for MapTileDownloader to perform the download in a way that makes use of MySSLSocketFactory. Let's call this MyTileDownloader.

  2. Create a drop-in replacement class for MapTileProviderBasic that instantiates your custom MyTileDownloader. Let's call this MyTileProvider.

  3. Instantiate your tile source as a new XYTileSource (no need to write a custom class).

  4. Instantiate MyTileProvider with your tile source instance.

  5. Instantiate MapVew with your tile provider instance.


MySSLSocketFactory is left as an exercise for the reader. See this post.


MyTileDownloader looks something like this:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.UnknownHostException;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.MapTileRequestState;
import org.osmdroid.tileprovider.modules.IFilesystemCache;
import org.osmdroid.tileprovider.modules.INetworkAvailablityCheck;
import org.osmdroid.tileprovider.modules.MapTileDownloader;
import org.osmdroid.tileprovider.modules.MapTileModuleProviderBase;
import org.osmdroid.tileprovider.tilesource.BitmapTileSourceBase.LowMemoryException;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase;
import org.osmdroid.tileprovider.util.StreamUtils;

import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;

/**
 * A drop-in replacement for {@link MapTileDownloader}. This loads tiles from an
 * HTTP or HTTPS server, making use of a custom {@link SSLSocketFactory} for SSL
 * peer verification.
 */
public class MyTileDownloader extends MapTileModuleProviderBase {
    private static final String TAG = "MyMapTileDownloader";

    protected OnlineTileSourceBase mTileSource;
    protected final IFilesystemCache mFilesystemCache;
    protected final INetworkAvailablityCheck mNetworkAvailablityCheck;
    protected final SSLSocketFactory mSSLSocketFactory;

    public MyTileDownloader(ITileSource pTileSource,
            IFilesystemCache pFilesystemCache,
            INetworkAvailablityCheck pNetworkAvailablityCheck,
            SSLSocketFactory pSSLSocketFactory) {
        super(4, TILE_DOWNLOAD_MAXIMUM_QUEUE_SIZE);
        setTileSource(pTileSource);
        mFilesystemCache = pFilesystemCache;
        mNetworkAvailablityCheck = pNetworkAvailablityCheck;
        mSSLSocketFactory = pSSLSocketFactory;
    }

    public ITileSource getTileSource() {
        return mTileSource;
    }

    @Override
    public void setTileSource(final ITileSource tileSource) {
        // We are only interested in OnlineTileSourceBase tile sources
        if (tileSource instanceof OnlineTileSourceBase)
            mTileSource = (OnlineTileSourceBase) tileSource;
        else
            mTileSource = null;
    }

    @Override
    public boolean getUsesDataConnection() {
        return true;
    }

    @Override
    protected String getName() {
        return "Online Tile Download Provider";
    }

    @Override
    protected String getThreadGroupName() {
        return "downloader";
    }

    @Override
    public int getMinimumZoomLevel() {
        return (mTileSource != null ? mTileSource.getMinimumZoomLevel()
                : MINIMUM_ZOOMLEVEL);
    }

    @Override
    public int getMaximumZoomLevel() {
        return (mTileSource != null ? mTileSource.getMaximumZoomLevel()
                : MAXIMUM_ZOOMLEVEL);
    }

    @Override
    protected Runnable getTileLoader() {
        return new TileLoader();
    };

    private class TileLoader extends MapTileModuleProviderBase.TileLoader {
        @Override
        public Drawable loadTile(final MapTileRequestState aState)
                throws CantContinueException {
            if (mTileSource == null)
                return null;

            InputStream in = null;
            OutputStream out = null;
            final MapTile tile = aState.getMapTile();

            try {
                if (mNetworkAvailablityCheck != null
                        && !mNetworkAvailablityCheck.getNetworkAvailable()) {
                    if (DEBUGMODE)
                        Log.d(TAG, "Skipping " + getName()
                                + " due to NetworkAvailabliltyCheck.");
                    return null;
                }

                final String tileURLString = mTileSource.getTileURLString(tile);
                if (DEBUGMODE)
                    Log.d(TAG, "Downloading Maptile from url: " + tileURLString);

                if (TextUtils.isEmpty(tileURLString))
                    return null;

                // Create an HttpURLConnection to download the tile
                URL url = new URL(tileURLString);
                HttpURLConnection connection = (HttpURLConnection) url
                        .openConnection();
                connection.setConnectTimeout(30000);
                connection.setReadTimeout(30000);

                // Use our custom SSLSocketFactory for secure connections
                if ("https".equalsIgnoreCase(url.getProtocol()))
                    ((HttpsURLConnection) connection)
                            .setSSLSocketFactory(mSSLSocketFactory);

                // Open the input stream
                in = new BufferedInputStream(connection.getInputStream(),
                        StreamUtils.IO_BUFFER_SIZE);

                // Check to see if we got success
                if (connection.getResponseCode() != 200) {
                    Log.w(TAG, "Problem downloading MapTile: " + tile
                            + " HTTP response: " + connection.getHeaderField(0));
                    return null;
                }

                // Read the tile into an in-memory byte array
                final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
                out = new BufferedOutputStream(dataStream,
                        StreamUtils.IO_BUFFER_SIZE);
                StreamUtils.copy(in, out);
                out.flush();
                final byte[] data = dataStream.toByteArray();
                final ByteArrayInputStream byteStream = new ByteArrayInputStream(
                        data);

                // Save the data to the filesystem cache
                if (mFilesystemCache != null) {
                    mFilesystemCache.saveFile(mTileSource, tile, byteStream);
                    byteStream.reset();
                }
                final Drawable result = mTileSource.getDrawable(byteStream);
                return result;

            } catch (final UnknownHostException e) {
                Log.w(TAG, "UnknownHostException downloading MapTile: " + tile
                        + " : " + e);
                throw new CantContinueException(e);

            } catch (final LowMemoryException e) {
                Log.w(TAG, "LowMemoryException downloading MapTile: " + tile
                        + " : " + e);
                throw new CantContinueException(e);

            } catch (final FileNotFoundException e) {
                Log.w(TAG, "Tile not found: " + tile + " : " + e);

            } catch (final IOException e) {
                Log.w(TAG, "IOException downloading MapTile: " + tile + " : "
                        + e);

            } catch (final Throwable e) {
                Log.e(TAG, "Error downloading MapTile: " + tile, e);

            } finally {
                StreamUtils.closeStream(in);
                StreamUtils.closeStream(out);
            }
            return null;
        }

        @Override
        protected void tileLoaded(final MapTileRequestState pState,
                final Drawable pDrawable) {
            // Don't return the tile Drawable because we'll wait for the fs
            // provider to ask for it. This prevent flickering when a load
            // of delayed downloads complete for tiles that we might not
            // even be interested in any more.
            super.tileLoaded(pState, null);
        }
    }
}

MyTileProvider looks something like this.

Note that you'll need a way to get access to your instance of MySSLSocketFactory inside this class. This is left as an exercise for the reader. I did this using app.getSSLSocketFactory(), where app is an instance of a custom class that extends Application, but your mileage may vary.

import javax.net.ssl.SSLSocketFactory;

import org.osmdroid.tileprovider.IMapTileProviderCallback;
import org.osmdroid.tileprovider.IRegisterReceiver;
import org.osmdroid.tileprovider.MapTileProviderArray;
import org.osmdroid.tileprovider.MapTileProviderBasic;
import org.osmdroid.tileprovider.modules.INetworkAvailablityCheck;
import org.osmdroid.tileprovider.modules.MapTileFileArchiveProvider;
import org.osmdroid.tileprovider.modules.MapTileFilesystemProvider;
import org.osmdroid.tileprovider.modules.NetworkAvailabliltyCheck;
import org.osmdroid.tileprovider.modules.TileWriter;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.util.SimpleRegisterReceiver;

import android.content.Context;

/**
 * A drop-in replacement for {@link MapTileProviderBasic}. This top-level tile
 * provider implements a basic tile request chain which includes a
 * {@link MapTileFilesystemProvider} (a file-system cache), a
 * {@link MapTileFileArchiveProvider} (archive provider), and a
 * {@link MyTileDownloader} (downloads map tiles via tile source).
 */
public class MyTileProvider extends MapTileProviderArray implements
        IMapTileProviderCallback {
    public MyTileProvider(final Context pContext, final ITileSource pTileSource) {
        this(new SimpleRegisterReceiver(pContext),
                new NetworkAvailabliltyCheck(pContext), pTileSource, app
                        .getSSLSocketFactory());
    }

    protected MyTileProvider(final IRegisterReceiver pRegisterReceiver,
            final INetworkAvailablityCheck aNetworkAvailablityCheck,
            final ITileSource pTileSource,
            final SSLSocketFactory pSSLSocketFactory) {
        super(pTileSource, pRegisterReceiver);

        // Look for raw tiles on the file system
        final MapTileFilesystemProvider fileSystemProvider = new MapTileFilesystemProvider(
                pRegisterReceiver, pTileSource);
        mTileProviderList.add(fileSystemProvider);

        // Look for tile archives on the file system
        final MapTileFileArchiveProvider archiveProvider = new MapTileFileArchiveProvider(
                pRegisterReceiver, pTileSource);
        mTileProviderList.add(archiveProvider);

        // Look for raw tiles on the Internet
        final TileWriter tileWriter = new TileWriter();
        final MyTileDownloader downloaderProvider = new MyTileDownloader(
                pTileSource, tileWriter, aNetworkAvailablityCheck,
                pSSLSocketFactory);
        mTileProviderList.add(downloaderProvider);
    }
}

Finally, the instantiation looks something like this:

XYTileSource tileSource = new XYTileSource("MapQuest", null, 3, 8, 256, ".jpg",
    "https://10.0.0.1/path/to/your/map/tiles/");
MapTileProviderBase tileProvider = new MyTileProvider(context, tileSource);
ResourceProxy resourceProxy = new DefaultResourceProxyImpl(context);
MapView mapView = new MapView(context, 256, resourceProxy, tileProvider);
Community
  • 1
  • 1
quietmint
  • 13,885
  • 6
  • 48
  • 73
0

I don't use osmdroid, but unless it has public interface to replace the downloader class(es), your best bet is to get the source and patch it to make it configurable or use your own downloader class. If MapTileDownloader implements some interface you could probably do some reflection voodoo to replace it at runtime, but that might have unknown side effects.

Nikolay Elenkov
  • 52,576
  • 10
  • 84
  • 84