add support for RFC7711 to MTM
This commit is contained in:
parent
1e7b4030bb
commit
cbc9c1fb20
|
@ -7,14 +7,14 @@ buildscript {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'android-library'
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 19
|
compileSdkVersion 24
|
||||||
buildToolsVersion "19.1"
|
buildToolsVersion "23.0.3"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 7
|
minSdkVersion 14
|
||||||
targetSdkVersion 19
|
targetSdkVersion 24
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|
|
@ -35,15 +35,32 @@ import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.cert.*;
|
import java.security.cert.*;
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
@ -53,6 +70,7 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
@ -68,7 +86,7 @@ import javax.net.ssl.X509TrustManager;
|
||||||
* <b>WARNING:</b> This only works if a dedicated thread is used for
|
* <b>WARNING:</b> This only works if a dedicated thread is used for
|
||||||
* opening sockets!
|
* opening sockets!
|
||||||
*/
|
*/
|
||||||
public class MemorizingTrustManager implements X509TrustManager {
|
public class MemorizingTrustManager {
|
||||||
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
||||||
final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
||||||
final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
||||||
|
@ -94,6 +112,7 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
private KeyStore appKeyStore;
|
private KeyStore appKeyStore;
|
||||||
private X509TrustManager defaultTrustManager;
|
private X509TrustManager defaultTrustManager;
|
||||||
private X509TrustManager appTrustManager;
|
private X509TrustManager appTrustManager;
|
||||||
|
private String poshCacheDir;
|
||||||
|
|
||||||
/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
|
/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
|
||||||
*
|
*
|
||||||
|
@ -149,28 +168,11 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
|
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
|
||||||
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
|
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
|
||||||
|
|
||||||
|
poshCacheDir = app.getFilesDir().getAbsolutePath()+"/posh_cache/";
|
||||||
|
|
||||||
appKeyStore = loadAppKeyStore();
|
appKeyStore = loadAppKeyStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a X509TrustManager list containing a new instance of
|
|
||||||
* TrustManagerFactory.
|
|
||||||
*
|
|
||||||
* This function is meant for convenience only. You can use it
|
|
||||||
* as follows to integrate TrustManagerFactory for HTTPS sockets:
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* SSLContext sc = SSLContext.getInstance("TLS");
|
|
||||||
* sc.init(null, MemorizingTrustManager.getInstanceList(this),
|
|
||||||
* new java.security.SecureRandom());
|
|
||||||
* HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
|
||||||
* </pre>
|
|
||||||
* @param c Activity or Service to show the Dialog / Notification
|
|
||||||
*/
|
|
||||||
public static X509TrustManager[] getInstanceList(Context c) {
|
|
||||||
return new X509TrustManager[] { new MemorizingTrustManager(c) };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds an Activity to the MTM for displaying the query dialog.
|
* Binds an Activity to the MTM for displaying the query dialog.
|
||||||
|
@ -389,7 +391,7 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive)
|
public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
|
||||||
throws CertificateException
|
throws CertificateException
|
||||||
{
|
{
|
||||||
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
||||||
|
@ -419,6 +421,14 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
else
|
else
|
||||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
|
if (domain != null && isServer) {
|
||||||
|
String hash = getBase64Hash(chain[0],"SHA-256");
|
||||||
|
List<String> fingerprints = getPoshFingerprints(domain);
|
||||||
|
if (hash != null && fingerprints.contains(hash)) {
|
||||||
|
Log.d("mtm","trusted cert fingerprint of "+domain+" via posh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
interactCert(chain, authType, e);
|
interactCert(chain, authType, e);
|
||||||
|
@ -429,20 +439,121 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
private List<String> getPoshFingerprints(String domain) {
|
||||||
throws CertificateException
|
List<String> cached = getPoshFingerprintsFromCache(domain);
|
||||||
{
|
if (cached == null) {
|
||||||
checkCertTrusted(chain, authType, false,true);
|
return getPoshFingerprintsFromServer(domain);
|
||||||
|
} else {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
private List<String> getPoshFingerprintsFromServer(String domain) {
|
||||||
throws CertificateException
|
try {
|
||||||
{
|
List<String> results = new ArrayList<>();
|
||||||
checkCertTrusted(chain, authType, true,true);
|
URL url = new URL("https://"+domain+"/.well-known/posh/xmpp-client.json");
|
||||||
|
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||||
|
String inputLine;
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
while ((inputLine = in.readLine()) != null) {
|
||||||
|
builder.append(inputLine);
|
||||||
|
}
|
||||||
|
JSONObject jsonObject = new JSONObject(builder.toString());
|
||||||
|
in.close();
|
||||||
|
JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
|
||||||
|
for(int i = 0; i < fingerprints.length(); i++) {
|
||||||
|
JSONObject fingerprint = fingerprints.getJSONObject(i);
|
||||||
|
String sha256 = fingerprint.getString("sha-256");
|
||||||
|
if (sha256 != null) {
|
||||||
|
results.add(sha256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int expires = jsonObject.getInt("expires");
|
||||||
|
if (expires <= 0) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
writeFingerprintsToCache(domain, results,1000L * expires+System.currentTimeMillis());
|
||||||
|
return results;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.d("mtm","error fetching posh "+e.getMessage());
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public X509Certificate[] getAcceptedIssuers()
|
private File getPoshCacheFile(String domain) {
|
||||||
{
|
return new File(poshCacheDir+domain+".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
|
||||||
|
File file = getPoshCacheFile(domain);
|
||||||
|
file.getParentFile().mkdirs();
|
||||||
|
try {
|
||||||
|
file.createNewFile();
|
||||||
|
JSONObject jsonObject = new JSONObject();
|
||||||
|
jsonObject.put("expires",expires);
|
||||||
|
jsonObject.put("fingerprints",new JSONArray(results));
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(file);
|
||||||
|
outputStream.write(jsonObject.toString().getBytes());
|
||||||
|
outputStream.flush();
|
||||||
|
outputStream.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getPoshFingerprintsFromCache(String domain) {
|
||||||
|
File file = getPoshCacheFile(domain);
|
||||||
|
try {
|
||||||
|
InputStream is = new FileInputStream(file);
|
||||||
|
BufferedReader buf = new BufferedReader(new InputStreamReader(is));
|
||||||
|
|
||||||
|
String line = buf.readLine();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
while(line != null){
|
||||||
|
sb.append(line).append("\n");
|
||||||
|
line = buf.readLine();
|
||||||
|
}
|
||||||
|
JSONObject jsonObject = new JSONObject(sb.toString());
|
||||||
|
is.close();
|
||||||
|
long expires = jsonObject.getLong("expires");
|
||||||
|
long expiresIn = expires - System.currentTimeMillis();
|
||||||
|
if (expiresIn < 0) {
|
||||||
|
file.delete();
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
Log.d("mtm","posh fingerprints expire in "+(expiresIn/1000)+"s");
|
||||||
|
}
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
|
||||||
|
for(int i = 0; i < jsonArray.length(); ++i) {
|
||||||
|
result.add(jsonArray.getString(i));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
file.delete();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException {
|
||||||
|
MessageDigest md;
|
||||||
|
try {
|
||||||
|
md = MessageDigest.getInstance(digest);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
md.update(certificate.getEncoded());
|
||||||
|
return Base64.encodeToString(md.digest(),Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate[] getAcceptedIssuers() {
|
||||||
LOGGER.log(Level.FINE, "getAcceptedIssuers()");
|
LOGGER.log(Level.FINE, "getAcceptedIssuers()");
|
||||||
return defaultTrustManager.getAcceptedIssuers();
|
return defaultTrustManager.getAcceptedIssuers();
|
||||||
}
|
}
|
||||||
|
@ -553,22 +664,6 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
certDetails(si, cert);
|
certDetails(si, cert);
|
||||||
return si.toString();
|
return si.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can use Notification.Builder once MTM's minSDK is >= 11
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
void startActivityNotification(Intent intent, int decisionId, String certName) {
|
|
||||||
Notification n = new Notification(android.R.drawable.ic_lock_lock,
|
|
||||||
master.getString(R.string.mtm_notification),
|
|
||||||
System.currentTimeMillis());
|
|
||||||
PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
|
|
||||||
n.setLatestEventInfo(master.getApplicationContext(),
|
|
||||||
master.getString(R.string.mtm_notification),
|
|
||||||
certName, call);
|
|
||||||
n.flags |= Notification.FLAG_AUTO_CANCEL;
|
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID + decisionId, n);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the top-most entry of the activity stack.
|
* Returns the top-most entry of the activity stack.
|
||||||
*
|
*
|
||||||
|
@ -598,7 +693,6 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
getUI().startActivity(ni);
|
getUI().startActivity(ni);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
|
LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
|
||||||
startActivityNotification(ni, myId, message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -708,22 +802,39 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public X509TrustManager getNonInteractive(String domain) {
|
||||||
|
return new NonInteractiveMemorizingTrustManager(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509TrustManager getInteractive(String domain) {
|
||||||
|
return new InteractiveMemorizingTrustManager(domain);
|
||||||
|
}
|
||||||
|
|
||||||
public X509TrustManager getNonInteractive() {
|
public X509TrustManager getNonInteractive() {
|
||||||
return new NonInteractiveMemorizingTrustManager();
|
return new NonInteractiveMemorizingTrustManager(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509TrustManager getInteractive() {
|
||||||
|
return new InteractiveMemorizingTrustManager(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
|
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
|
||||||
|
|
||||||
|
private final String domain;
|
||||||
|
|
||||||
|
public NonInteractiveMemorizingTrustManager(String domain) {
|
||||||
|
this.domain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||||
throws CertificateException {
|
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
|
||||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||||
throws CertificateException {
|
throws CertificateException {
|
||||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
|
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -732,4 +843,28 @@ public class MemorizingTrustManager implements X509TrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class InteractiveMemorizingTrustManager implements X509TrustManager {
|
||||||
|
private final String domain;
|
||||||
|
|
||||||
|
public InteractiveMemorizingTrustManager(String domain) {
|
||||||
|
this.domain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||||
|
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||||
|
throws CertificateException {
|
||||||
|
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return MemorizingTrustManager.this.getAcceptedIssuers();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
||||||
final X509TrustManager trustManager;
|
final X509TrustManager trustManager;
|
||||||
final HostnameVerifier hostnameVerifier;
|
final HostnameVerifier hostnameVerifier;
|
||||||
if (interactive) {
|
if (interactive) {
|
||||||
trustManager = mXmppConnectionService.getMemorizingTrustManager();
|
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
|
||||||
hostnameVerifier = mXmppConnectionService
|
hostnameVerifier = mXmppConnectionService
|
||||||
.getMemorizingTrustManager().wrapHostnameVerifier(
|
.getMemorizingTrustManager().wrapHostnameVerifier(
|
||||||
new StrictHostnameVerifier());
|
new StrictHostnameVerifier());
|
||||||
|
|
|
@ -1666,7 +1666,7 @@ public class XmppConnectionService extends Service {
|
||||||
callback.onAccountCreated(account);
|
callback.onAccountCreated(account);
|
||||||
if (Config.X509_VERIFICATION) {
|
if (Config.X509_VERIFICATION) {
|
||||||
try {
|
try {
|
||||||
getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
|
getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
callback.informUser(R.string.certificate_chain_is_not_trusted);
|
callback.informUser(R.string.certificate_chain_is_not_trusted);
|
||||||
}
|
}
|
||||||
|
@ -1694,7 +1694,7 @@ public class XmppConnectionService extends Service {
|
||||||
databaseBackend.updateAccount(account);
|
databaseBackend.updateAccount(account);
|
||||||
if (Config.X509_VERIFICATION) {
|
if (Config.X509_VERIFICATION) {
|
||||||
try {
|
try {
|
||||||
getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
|
getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
|
showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
|
||||||
}
|
}
|
||||||
|
|
|
@ -495,7 +495,8 @@ public class XmppConnection implements Runnable {
|
||||||
} else {
|
} else {
|
||||||
keyManager = null;
|
keyManager = null;
|
||||||
}
|
}
|
||||||
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG());
|
String domain = account.getJid().getDomainpart();
|
||||||
|
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
|
||||||
final SSLSocketFactory factory = sc.getSocketFactory();
|
final SSLSocketFactory factory = sc.getSocketFactory();
|
||||||
final HostnameVerifier verifier;
|
final HostnameVerifier verifier;
|
||||||
if (mInteractive) {
|
if (mInteractive) {
|
||||||
|
|
Loading…
Reference in New Issue