Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Ignore MacOS .DS_Store files
.DS_Store
.gradle
.dart_tool
pubspec.lock
2 changes: 2 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ rootProject.allprojects {
apply plugin: 'com.android.library'

android {
namespace 'com.criticalblue.approov_service_flutter_httpclient'

compileSdkVersion 29

defaultConfig {
Expand Down
3 changes: 1 addition & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.criticalblue.approov_service_flutter_httpclient">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import javax.net.ssl.HttpsURLConnection;

Expand All @@ -44,30 +46,125 @@
// presented on any particular URL to implement the pinning. Note that the MethodChannel must run on a background
// thread since it makes blocking calls.
public class ApproovHttpClientPlugin implements FlutterPlugin, MethodCallHandler {
// CertificatePrefetcher is a Runnable that fetches the certificates for a given URL. This allows the
// certificates to be fetched on a background thread in parallel with other fetches, and with an Approov
// token fetch.
private class CertificatePrefetcher implements Runnable {
// Connect timeout (in ms) for host certificate fetch
private static final int FETCH_CERTIFICATES_TIMEOUT_MS = 3000;

// URL being probed for the certificates
private final URL url;

// Latch to signal completion of the certificate fetch
private final CountDownLatch countDownLatch;

// Any exception from the fetching process, or null otherwise
private Exception exception;

// The host certificates that were fetched, or null if there was an error
private final List<byte[]> hostCertificates;

/**
* Constructor for the CertificatePrefetcher, which sets up ready for ansynchronous
* execution.
*
* @param url The URL to fetch the certificates from.
*/
public CertificatePrefetcher(URL url) {
this.url = url;
hostCertificates = new ArrayList<>();
exception = null;
countDownLatch = new CountDownLatch(1);
}

/**
* Runs the prefetcher to fetch the certificates from the URL.
*/
@Override
public void run() {
try {
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setConnectTimeout(FETCH_CERTIFICATES_TIMEOUT_MS);
connection.connect();
Certificate[] certificates = connection.getServerCertificates();
for (Certificate certificate: certificates) {
hostCertificates.add(certificate.getEncoded());
}
connection.disconnect();
countDownLatch.countDown();
} catch (Exception e) {
exception = e;
countDownLatch.countDown();
}
}

/**
* Waits for the certificate fetch to complete and returns the exception if there was one.
*
* @return The exception from the fetch, or nil if there was no exception
*/
public Exception getResultException() {
try {
countDownLatch.await();
return exception;
} catch (InterruptedException e) {
return e;
}
}

/**
* Returns the host certificates that were fetched. Note that this should only be called after
* getResultException to make sure the results have been waited on.
*
* @return The host certificates that were fetched.
*/
public List<byte[]> getResultCertificates() {
return hostCertificates;
}
}

/**
* Utility class for providing a callback handler for receiving a token fetch result on an
* internal asynchronous request. This is for the prefetch case where the actual result is
* not required.
*/
private class InternalCallbackHandler implements Approov.TokenFetchCallback {
/**
* Construct a new internal callback handler.
*/
InternalCallbackHandler() {
}

@Override
public void approovCallback(Approov.TokenFetchResult pResult) {
}
}

// The MethodChannel for the communication between Flutter and native Android
//
// This local reference serves to register the plugin with the Flutter Engine and unregister it
// when the Flutter Engine is detached from the Activity
private MethodChannel channel;

// Connect timeout (in ms) for host certificate fetch
private static final int FETCH_CERTIFICATES_TIMEOUT_MS = 3000;

// Application context passed to Approov initialization
private static Context appContext;

// Provides any prior initial configuration supplied, to allow a reinitialization caused by
// a hot restart if the configuration is the same
private static String initializedConfig;

// Map of hostname to current in-flight certificate prefetchers
private static Map<String, CertificatePrefetcher> certificatePrefetchers;

@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
BinaryMessenger messenger = flutterPluginBinding.getBinaryMessenger();
channel = new MethodChannel(messenger, "approov_service_flutter_httpclient",
StandardMethodCodec.INSTANCE, messenger.makeBackgroundTaskQueue());
channel.setMethodCallHandler(this);
appContext = flutterPluginBinding.getApplicationContext();
certificatePrefetchers = new HashMap<>();
}

@Override
Expand Down Expand Up @@ -107,6 +204,13 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
} catch(Exception e) {
result.error("Approov.getPins", e.getLocalizedMessage(), null);
}
} else if (call.method.equals("fetchApproovToken")) {
try {
Approov.fetchApproovToken(new InternalCallbackHandler(), call.argument("url"));
result.success(null);
} catch(Exception e) {
result.error("Approov.fetchApproovToken", e.getLocalizedMessage(), null);
}
} else if (call.method.equals("fetchApproovTokenAndWait")) {
try {
Approov.TokenFetchResult tokenFetchResult = Approov.fetchApproovTokenAndWait(call.argument("url"));
Expand Down Expand Up @@ -152,21 +256,38 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
} catch(Exception e) {
result.error("Approov.setUserProperty", e.getLocalizedMessage(), null);
}
} else if (call.method.equals("prefetchHostCertificates")) {
try {
final URL url = new URL(call.argument("url"));
CertificatePrefetcher certPrefetcher = certificatePrefetchers.get(url.getHost());
if (certPrefetcher == null) {
certPrefetcher = new CertificatePrefetcher(url);
certificatePrefetchers.put(url.getHost(), certPrefetcher);
new Thread(certPrefetcher).start();
}
result.success(null);
} catch(Exception e) {
result.error("Approov.prefetchHostCertificates", e.getLocalizedMessage(), null);
}
} else if (call.method.equals("fetchHostCertificates")) {
try {
final URL url = new URL(call.argument("url"));
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setConnectTimeout(FETCH_CERTIFICATES_TIMEOUT_MS);
connection.connect();
Certificate[] certificates = connection.getServerCertificates();
final List<byte[]> hostCertificates = new ArrayList<>(certificates.length);
for (Certificate certificate: certificates) {
hostCertificates.add(certificate.getEncoded());
CertificatePrefetcher certPrefetcher = certificatePrefetchers.get(url.getHost());
if (certPrefetcher == null) {
certPrefetcher = new CertificatePrefetcher(url);
new Thread(certPrefetcher).start();
} else {
certificatePrefetchers.remove(url.getHost());
}
connection.disconnect();
result.success(hostCertificates);
} catch (Exception e) {
result.error("fetchHostCertificates", e.getLocalizedMessage(), null);
Exception exception = certPrefetcher.getResultException();
if (exception != null) {
result.error("Approov.fetchHostCertificates", exception.getLocalizedMessage(), null);
} else {
List<byte[]> hostCertificates = certPrefetcher.getResultCertificates();
result.success(hostCertificates);
}
} catch(Exception e) {
result.error("Approov.fetchHostCertificatesy", e.getLocalizedMessage(), null);
}
} else if (call.method.equals("fetchSecureStringAndWait")) {
try {
Expand Down
4 changes: 0 additions & 4 deletions ios/Classes/ApproovHttpClientPlugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,4 @@

@interface ApproovHttpClientPlugin: NSObject<FlutterPlugin>

// Provides any prior initial configuration supplied, to allow a reinitialization caused by
// a hot restart if the configuration is the same
@property NSString *initializedConfig;

@end
Loading