diff options
author | Chris Warrington <cmw@google.com> | 2016-10-18 12:29:21 +0100 |
---|---|---|
committer | Chris Warrington <cmw@google.com> | 2016-10-18 12:34:18 +0100 |
commit | e3780081075c01aa1dff6d1f373cb43192b33e68 (patch) | |
tree | fb734615933a39f3d009210dc0d1457160479b35 /WordPress/src/main/java/org/wordpress/android/networking | |
parent | 7e05eb7e57827eddc885570bc00aed8a50320dbf (diff) | |
parent | 025b8b226c8d8edba2b309ca878572f40512eca7 (diff) | |
download | gradle-perf-android-medium-e3780081075c01aa1dff6d1f373cb43192b33e68.tar.gz |
Merge remote-tracking branch 'origin/upstream-master' into masterHEADstudio-3.4.0studio-3.2.1studio-3.1.2studio-3.0studio-2.3gradle_3.4.0gradle_3.1.2gradle_3.0.0gradle_2.3.0studio-master-devmirror-goog-studio-master-devmastermain
Change-Id: I63f5e16d09297c48432192761b840310935eb903
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/networking')
11 files changed, 1015 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java b/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java new file mode 100644 index 000000000..238f10fc6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/ConnectionChangeReceiver.java @@ -0,0 +1,70 @@ +package org.wordpress.android.networking; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; + +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.NetworkUtils; + +import de.greenrobot.event.EventBus; + +/* + * global network connection change receiver - declared in the manifest to monitor + * android.net.conn.CONNECTIVITY_CHANGE + */ +public class ConnectionChangeReceiver extends BroadcastReceiver { + private static boolean mIsFirstReceive = true; + private static boolean mWasConnected = true; + private static boolean mIsEnabled = false; // this value must be synchronized with the ConnectionChangeReceiver + // state in our AndroidManifest + + public static class ConnectionChangeEvent { + private final boolean mIsConnected; + public ConnectionChangeEvent(boolean isConnected) { + mIsConnected = isConnected; + } + public boolean isConnected() { + return mIsConnected; + } + } + + /* + * note that onReceive occurs when anything about the connection has changed, not just + * when the connection has been lost or restated, so it can happen quite often when the + * user is on the move. for this reason we only fire the event the first time onReceive + * is called, and afterwards only when we know connection availability has changed + */ + @Override + public void onReceive(Context context, Intent intent) { + boolean isConnected = NetworkUtils.isNetworkAvailable(context); + if (mIsFirstReceive || isConnected != mWasConnected) { + postConnectionChangeEvent(isConnected); + } + } + + private static void postConnectionChangeEvent(boolean isConnected) { + AppLog.i(T.UTILS, "Connection status changed, isConnected=" + isConnected); + mWasConnected = isConnected; + mIsFirstReceive = false; + EventBus.getDefault().post(new ConnectionChangeEvent(isConnected)); + } + + public static void setEnabled(Context context, boolean enabled) { + if (mIsEnabled == enabled) { + return; + } + mIsEnabled = enabled; + AppLog.i(T.UTILS, "ConnectionChangeReceiver.setEnabled " + enabled); + int flag = (enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : + PackageManager.COMPONENT_ENABLED_STATE_DISABLED); + ComponentName component = new ComponentName(context, ConnectionChangeReceiver.class); + context.getPackageManager().setComponentEnabledSetting(component, flag, PackageManager.DONT_KILL_APP); + if (mIsEnabled) { + postConnectionChangeEvent(NetworkUtils.isNetworkAvailable(context)); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java b/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java new file mode 100644 index 000000000..0ed52a0a8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/GravatarApi.java @@ -0,0 +1,130 @@ +package org.wordpress.android.networking; + +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.models.AccountHelper; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.CrashlyticsUtils; + +import android.os.Handler; +import android.os.Looper; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Interceptor; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class GravatarApi { + public static final String API_BASE_URL = "https://api.gravatar.com/v1/"; + + public interface GravatarUploadListener { + void onSuccess(); + void onError(); + } + + private static OkHttpClient createClient(final String restEndpointUrl) { + OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder(); + + //// uncomment the following line to add logcat logging + //httpClientBuilder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)); + + // add oAuth token usage + httpClientBuilder.addInterceptor(new Interceptor() { + @Override + public Response intercept(Interceptor.Chain chain) throws IOException { + Request original = chain.request(); + + String siteId = AuthenticatorRequest.extractSiteIdFromUrl(restEndpointUrl, original.url() + .toString()); + String token = OAuthAuthenticator.getAccessToken(siteId); + + Request.Builder requestBuilder = original.newBuilder() + .header("Authorization", "Bearer " + token) + .method(original.method(), original.body()); + + Request request = requestBuilder.build(); + return chain.proceed(request); + } + }); + + return httpClientBuilder.build(); + } + + public static Request prepareGravatarUpload(String email, File file) { + return new Request.Builder() + .url(API_BASE_URL + "upload-image") + .post(new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("account", email) + .addFormDataPart("filedata", file.getName(), new StreamingRequest(file)) + .build()) + .build(); + } + + public static void uploadGravatar(final File file, final GravatarUploadListener gravatarUploadListener) { + Request request = prepareGravatarUpload(AccountHelper.getDefaultAccount().getEmail(), file); + + createClient(API_BASE_URL).newCall(request).enqueue( + new Callback() { + @Override + public void onResponse(Call call, final Response response) throws IOException { + if (!response.isSuccessful()) { + Map<String, Object> properties = new HashMap<>(); + properties.put("network_response_code", response.code()); + + // response's body can only be read once so, keep it in a local variable + String responseBody; + + try { + responseBody = response.body().string(); + } catch (IOException e) { + responseBody = "null"; + } + properties.put("network_response_body", responseBody); + + AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_UNSUCCESSFUL, + properties); + AppLog.w(AppLog.T.API, "Network call unsuccessful trying to upload Gravatar: " + + responseBody); + } + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (response.isSuccessful()) { + gravatarUploadListener.onSuccess(); + } else { + gravatarUploadListener.onError(); + } + } + }); + } + + @Override + public void onFailure(okhttp3.Call call, final IOException e) { + Map<String, Object> properties = new HashMap<>(); + properties.put("network_exception_class", e != null ? e.getClass().getCanonicalName() : "null"); + properties.put("network_exception_message", e != null ? e.getMessage() : "null"); + AnalyticsTracker.track(AnalyticsTracker.Stat.ME_GRAVATAR_UPLOAD_EXCEPTION, properties); + CrashlyticsUtils.logException(e, CrashlyticsUtils.ExceptionType.SPECIFIC, + AppLog.T.API, "Network call failure trying to upload Gravatar!"); + AppLog.w(AppLog.T.API, "Network call failure trying to upload Gravatar!" + (e != null ? + e.getMessage() : "null")); + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + gravatarUploadListener.onError(); + } + }); + } + }); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticator.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticator.java new file mode 100644 index 000000000..326ca9064 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticator.java @@ -0,0 +1,35 @@ +package org.wordpress.android.networking; + +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.AccountHelper; +import org.wordpress.android.util.StringUtils; + +public class OAuthAuthenticator implements Authenticator { + public static String getAccessToken(final String siteId) { + String token = AccountHelper.getDefaultAccount().getAccessToken(); + + if (siteId != null) { + // Get the token for a Jetpack site if needed + Blog blog = WordPress.wpDB.getBlogForDotComBlogId(siteId); + + if (blog != null) { + String jetpackToken = blog.getApi_key(); + + // valid OAuth tokens are 64 chars + if (jetpackToken != null && jetpackToken.length() == 64 && !blog.isDotcomFlag()) { + token = jetpackToken; + } + } + } + + return token; + } + + @Override + public void authenticate(final AuthenticatorRequest request) { + String siteId = request.getSiteId(); + String token = getAccessToken(siteId); + request.sendWithAccessToken(StringUtils.notNullStr(token)); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactory.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactory.java new file mode 100644 index 000000000..5dc68edb6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactory.java @@ -0,0 +1,12 @@ +package org.wordpress.android.networking; + +public class OAuthAuthenticatorFactory { + private static OAuthAuthenticatorFactoryAbstract sFactory; + + public static OAuthAuthenticator instantiate() { + if (sFactory == null) { + sFactory = new OAuthAuthenticatorFactoryDefault(); + } + return sFactory.make(); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryAbstract.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryAbstract.java new file mode 100644 index 000000000..85cff768c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryAbstract.java @@ -0,0 +1,5 @@ +package org.wordpress.android.networking; + +public interface OAuthAuthenticatorFactoryAbstract { + public OAuthAuthenticator make(); +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryDefault.java b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryDefault.java new file mode 100644 index 000000000..4687d1dca --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/OAuthAuthenticatorFactoryDefault.java @@ -0,0 +1,7 @@ +package org.wordpress.android.networking; + +public class OAuthAuthenticatorFactoryDefault implements OAuthAuthenticatorFactoryAbstract { + public OAuthAuthenticator make() { + return new OAuthAuthenticator(); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/SSLCertsViewActivity.java b/WordPress/src/main/java/org/wordpress/android/networking/SSLCertsViewActivity.java new file mode 100644 index 000000000..c6914b889 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/SSLCertsViewActivity.java @@ -0,0 +1,42 @@ +package org.wordpress.android.networking; + +import android.os.Bundle; +import android.support.v7.app.ActionBar; + +import org.wordpress.android.R; +import org.wordpress.android.ui.WebViewActivity; + +/** + * Display details of a SSL cert + */ +public class SSLCertsViewActivity extends WebViewActivity { + public static final String CERT_DETAILS_KEYS = "CertDetails"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(getResources().getText(R.string.ssl_certificate_details)); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(false); + } + } + + @Override + protected void loadContent() { + Bundle extras = getIntent().getExtras(); + if (extras != null && extras.containsKey(CERT_DETAILS_KEYS)) { + String certDetails = extras.getString(CERT_DETAILS_KEYS); + StringBuilder sb = new StringBuilder("<html><body>"); + sb.append(certDetails); + sb.append("</body></html>"); + mWebView.loadDataWithBaseURL(null, sb.toString(), "text/html", "utf-8", null); + } + } + + @Override + protected void configureWebView() { + mWebView.getSettings().setDefaultTextEncodingName("utf-8"); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/SelfSignedSSLCertsManager.java b/WordPress/src/main/java/org/wordpress/android/networking/SelfSignedSSLCertsManager.java new file mode 100644 index 000000000..172530d51 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/SelfSignedSSLCertsManager.java @@ -0,0 +1,267 @@ +package org.wordpress.android.networking; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.http.SslCertificate; +import android.os.Bundle; + +import org.wordpress.android.BuildConfig; +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.ui.ActivityLauncher; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.GenericCallback; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import javax.security.auth.x500.X500Principal; + +public class SelfSignedSSLCertsManager { + private static SelfSignedSSLCertsManager sInstance; + private File mLocalTrustStoreFile; + private KeyStore mLocalKeyStore; + // Used to hold the last self-signed certificate chain that doesn't pass trusting + private X509Certificate[] mLastFailureChain; + + private SelfSignedSSLCertsManager(Context ctx) throws IOException, GeneralSecurityException { + mLocalTrustStoreFile = new File(ctx.getFilesDir(), "self_signed_certs_truststore.bks"); + createLocalKeyStoreFile(); + mLocalKeyStore = loadTrustStore(ctx); + } + + public static void askForSslTrust(final Context ctx, final GenericCallback<Void> certificateTrusted) { + AlertDialog.Builder alert = new AlertDialog.Builder(ctx); + alert.setTitle(ctx.getString(R.string.ssl_certificate_error)); + alert.setMessage(ctx.getString(R.string.ssl_certificate_ask_trust)); + alert.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + SelfSignedSSLCertsManager selfSignedSSLCertsManager; + try { + selfSignedSSLCertsManager = SelfSignedSSLCertsManager.getInstance(ctx); + X509Certificate[] certificates = selfSignedSSLCertsManager.getLastFailureChain(); + AppLog.i(T.NUX, "Add the following certificate to our Certificate Manager: " + + Arrays.toString(certificates)); + selfSignedSSLCertsManager.addCertificates(certificates); + } catch (GeneralSecurityException e) { + AppLog.e(T.API, e); + } catch (IOException e) { + AppLog.e(T.API, e); + } + if (certificateTrusted != null) { + certificateTrusted.callback(null); + } + } + } + ); + alert.setNeutralButton(R.string.ssl_certificate_details, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + ActivityLauncher.viewSSLCerts(ctx); + } + }); + alert.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }); + alert.show(); + } + + public static synchronized SelfSignedSSLCertsManager getInstance(Context ctx) + throws IOException, GeneralSecurityException { + if (sInstance == null) { + sInstance = new SelfSignedSSLCertsManager(ctx); + } + return sInstance; + } + + public void addCertificates(X509Certificate[] certs) throws IOException, GeneralSecurityException { + if (certs == null || certs.length == 0) { + return; + } + + for (X509Certificate cert : certs) { + String alias = hashName(cert.getSubjectX500Principal()); + mLocalKeyStore.setCertificateEntry(alias, cert); + } + saveTrustStore(); + // reset the Volley queue Otherwise new certs are not used + WordPress.setupVolleyQueue(); + } + + public void addCertificate(X509Certificate cert) throws IOException, GeneralSecurityException { + if (cert == null) { + return; + } + + String alias = hashName(cert.getSubjectX500Principal()); + mLocalKeyStore.setCertificateEntry(alias, cert); + saveTrustStore(); + } + + public KeyStore getLocalKeyStore() { + return mLocalKeyStore; + } + + private KeyStore loadTrustStore(Context ctx) throws IOException, GeneralSecurityException { + KeyStore localTrustStore = KeyStore.getInstance("BKS"); + InputStream in = new FileInputStream(mLocalTrustStoreFile); + try { + localTrustStore.load(in, BuildConfig.DB_SECRET.toCharArray()); + } finally { + in.close(); + } + return localTrustStore; + } + + private void saveTrustStore() throws IOException, GeneralSecurityException { + FileOutputStream out = null; + try { + out = new FileOutputStream(mLocalTrustStoreFile); + mLocalKeyStore.store(out, BuildConfig.DB_SECRET.toCharArray()); + } finally { + if (out!=null){ + try { + out.close(); + } catch (IOException e) { + AppLog.e(T.UTILS, e); + } + } + } + } + + /** + * Create an empty trust store file if missing + */ + private void createLocalKeyStoreFile() throws GeneralSecurityException, IOException { + if (!mLocalTrustStoreFile.exists()) { + FileOutputStream out = null; + try { + out = new FileOutputStream(mLocalTrustStoreFile); + KeyStore localTrustStore = KeyStore.getInstance("BKS"); + localTrustStore.load(null, BuildConfig.DB_SECRET.toCharArray()); + localTrustStore.store(out, BuildConfig.DB_SECRET.toCharArray()); + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + AppLog.e(T.UTILS, e); + } + } + } + } + } + + public void emptyLocalKeyStoreFile() { + if (mLocalTrustStoreFile.exists()) { + mLocalTrustStoreFile.delete(); + } + try { + createLocalKeyStoreFile(); + } catch (GeneralSecurityException e) { + AppLog.e(T.API, "Cannot create/initialize local Keystore", e); + } catch (IOException e) { + AppLog.e(T.API, "Cannot create/initialize local Keystore", e); + } + } + + private static String hashName(X500Principal principal) { + try { + byte[] digest = MessageDigest.getInstance("MD5").digest(principal.getEncoded()); + String result = Integer.toString(leInt(digest), 16); + if (result.length() > 8) { + StringBuilder buff = new StringBuilder(); + int padding = 8 - result.length(); + for (int i = 0; i < padding; i++) { + buff.append("0"); + } + buff.append(result); + + return buff.toString(); + } + + return result; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + private static int leInt(byte[] bytes) { + int offset = 0; + return ((bytes[offset++] & 0xff) << 0) + | ((bytes[offset++] & 0xff) << 8) + | ((bytes[offset++] & 0xff) << 16) + | ((bytes[offset] & 0xff) << 24); + } + + public X509Certificate[] getLastFailureChain() { + return mLastFailureChain; + } + + public void setLastFailureChain(X509Certificate[] lastFaiulreChain) { + mLastFailureChain = lastFaiulreChain; + } + + public String getLastFailureChainDescription() { + return (mLastFailureChain == null || mLastFailureChain.length == 0) ? "" : mLastFailureChain[0].toString(); + } + + public boolean isCertificateTrusted(SslCertificate cert){ + if (cert==null) + return false; + + Bundle bundle = SslCertificate.saveState(cert); + X509Certificate x509Certificate; + byte[] bytes = bundle.getByteArray("x509-certificate"); + if (bytes == null) { + AppLog.e(T.API, "Cannot load the SSLCertificate bytes from the bundle!"); + x509Certificate = null; + } else { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + Certificate certX509 = certFactory.generateCertificate(new ByteArrayInputStream(bytes)); + x509Certificate = (X509Certificate) certX509; + } catch (CertificateException e) { + AppLog.e(T.API, "Cannot generate the X509Certificate with the bytes provided", e); + x509Certificate = null; + } + } + + return isCertificateTrusted(x509Certificate); + } + + public boolean isCertificateTrusted(X509Certificate x509Certificate){ + if (x509Certificate==null) + return false; + + // Now I have an X509Certificate I can pass to an X509TrustManager for validation. + try { + String certificateAlias = this.getLocalKeyStore().getCertificateAlias(x509Certificate); + if(certificateAlias != null ) { + AppLog.w(T.API, "Current certificate " + x509Certificate.getSubjectDN().getName() +" is in KeyStore."); + return true; + } + } catch (KeyStoreException e) { + AppLog.e(T.API, "Cannot check if the certificate is in KeyStore. Seems that Keystore is not initialized.", e); + } + + AppLog.w(T.API, "Current certificate " + x509Certificate.getSubjectDN().getName() +" is NOT in KeyStore."); + return false; + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java b/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java new file mode 100644 index 000000000..60c6880fe --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/StreamingRequest.java @@ -0,0 +1,41 @@ +package org.wordpress.android.networking; + +import java.io.File; +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okhttp3.internal.Util; +import okio.BufferedSink; +import okio.Okio; +import okio.Source; + +public class StreamingRequest extends RequestBody { + public static final int CHUNK_SIZE = 2048; + + private final File mFile; + + public StreamingRequest(File file) { + mFile = file; + } + + @Override + public MediaType contentType() { + return MediaType.parse("multipart/form-data"); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + Source source = null; + try { + source = Okio.source(mFile); + + while (source.read(sink.buffer(), CHUNK_SIZE) != -1) { + sink.flush(); + } + } finally { + Util.closeQuietly(source); + } + } +}; + diff --git a/WordPress/src/main/java/org/wordpress/android/networking/WPDelayedHurlStack.java b/WordPress/src/main/java/org/wordpress/android/networking/WPDelayedHurlStack.java new file mode 100644 index 000000000..b0afaec72 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/WPDelayedHurlStack.java @@ -0,0 +1,287 @@ +package org.wordpress.android.networking; + +import android.content.Context; +import android.util.Base64; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Request.Method; +import com.android.volley.toolbox.HttpStack; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.message.BasicStatusLine; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.Blog; +import org.wordpress.android.models.AccountHelper; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; +import org.wordpress.android.util.UrlUtils; +import org.wordpress.android.util.WPUrlUtils; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +/** + * An {@link HttpStack} based on the code of {@link com.android.volley.toolbox.HurlStack} that internally + * uses a {@link HttpURLConnection}. + * + * This implementation of {@link HttpStack} internally initializes {@link SelfSignedSSLCertsManager} in a secondary + * thread since initialization could take a few seconds. + */ +public class WPDelayedHurlStack implements HttpStack { + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + + private SSLSocketFactory mSslSocketFactory; + private final Blog mCurrentBlog; + private final Context mCtx; + private final Object monitor = new Object(); + + public WPDelayedHurlStack(final Context ctx, final Blog currentBlog) { + mCurrentBlog = currentBlog; + mCtx = ctx; + + // initializes SelfSignedSSLCertsManager in a separate thread. + Thread sslContextInitializer = new Thread() { + @Override + public void run() { + try { + TrustManager[] trustAllowedCerts = new TrustManager[]{ + new WPTrustManager(SelfSignedSSLCertsManager.getInstance(ctx).getLocalKeyStore()) + }; + SSLContext context = SSLContext.getInstance("SSL"); + context.init(null, trustAllowedCerts, new SecureRandom()); + mSslSocketFactory = context.getSocketFactory(); + } catch (NoSuchAlgorithmException e) { + AppLog.e(T.API, e); + } catch (KeyManagementException e) { + AppLog.e(T.API, e); + } catch (GeneralSecurityException e) { + AppLog.e(T.API, e); + } catch (IOException e) { + AppLog.e(T.API, e); + } + } + }; + sslContextInitializer.start(); + } + + + private static boolean hasAuthorizationHeader(Request request) { + try { + if (request.getHeaders() != null && request.getHeaders().containsKey("Authorization")) { + return true; + } + } catch (AuthFailureError e) { + // nope + } + + return false; + } + + @Override + public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) + throws IOException, AuthFailureError { + if (request.getUrl() != null) { + if (!WPUrlUtils.isWordPressCom(request.getUrl()) && mCurrentBlog != null + && mCurrentBlog.hasValidHTTPAuthCredentials()) { + String creds = String.format("%s:%s", mCurrentBlog.getHttpuser(), mCurrentBlog.getHttppassword()); + String auth = "Basic " + Base64.encodeToString(creds.getBytes(), Base64.DEFAULT); + additionalHeaders.put("Authorization", auth); + } + + /** + * Add the Authorization header to access private WP.com files. + * + * Note: Additional headers have precedence over request headers, so add Authorization only it it's not already + * available in the request. + * + */ + if (WPUrlUtils.safeToAddWordPressComAuthToken(request.getUrl()) && mCtx != null + && AccountHelper.isSignedInWordPressDotCom() && !hasAuthorizationHeader(request)) { + additionalHeaders.put("Authorization", "Bearer " + AccountHelper.getDefaultAccount().getAccessToken()); + } + } + + additionalHeaders.put("User-Agent", WordPress.getUserAgent()); + + String url = request.getUrl(); + + // Ensure that an HTTPS request is made to wpcom when Authorization is set. + if (additionalHeaders.containsKey("Authorization") || hasAuthorizationHeader(request)) { + url = UrlUtils.makeHttps(url); + } + + HashMap<String, String> map = new HashMap<String, String>(); + map.putAll(request.getHeaders()); + map.putAll(additionalHeaders); + + URL parsedUrl = new URL(url); + HttpURLConnection connection = openConnection(parsedUrl, request); + for (String headerName : map.keySet()) { + connection.addRequestProperty(headerName, map.get(headerName)); + } + setConnectionParametersForRequest(connection, request); + // Initialize HttpResponse with data from the HttpURLConnection. + ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); + int responseCode = connection.getResponseCode(); + if (responseCode == -1) { + // -1 is returned by getResponseCode() if the response code could not be retrieved. + // Signal to the caller that something was wrong with the connection. + throw new IOException("Could not retrieve response code from HttpUrlConnection."); + } + StatusLine responseStatus = new BasicStatusLine(protocolVersion, + connection.getResponseCode(), connection.getResponseMessage()); + BasicHttpResponse response = new BasicHttpResponse(responseStatus); + response.setEntity(entityFromConnection(connection)); + for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) { + if (header.getKey() != null) { + Header h = new BasicHeader(header.getKey(), header.getValue().get(0)); + response.addHeader(h); + } + } + return response; + } + + /** + * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}. + * @param connection + * @return an HttpEntity populated with data from <code>connection</code>. + */ + private static HttpEntity entityFromConnection(HttpURLConnection connection) { + BasicHttpEntity entity = new BasicHttpEntity(); + InputStream inputStream; + try { + inputStream = connection.getInputStream(); + } catch (IOException ioe) { + inputStream = connection.getErrorStream(); + } + entity.setContent(inputStream); + entity.setContentLength(connection.getContentLength()); + entity.setContentEncoding(connection.getContentEncoding()); + entity.setContentType(connection.getContentType()); + return entity; + } + + /** + * Create an {@link HttpURLConnection} for the specified {@code url}. + */ + protected HttpURLConnection createConnection(URL url) throws IOException { + // Check that the custom SslSocketFactory is not null on HTTPS connections + if (UrlUtils.isHttps(url) && !WPUrlUtils.isWordPressCom(url) + && !WPUrlUtils.isGravatar(url)) { + // WordPress.com doesn't need the custom mSslSocketFactory + synchronized (monitor) { + while (mSslSocketFactory == null) { + try { + monitor.wait(500); + } catch (InterruptedException e) { + // we can't do much here. + } + } + } + } + + return (HttpURLConnection) url.openConnection(); + } + + /** + * Opens an {@link HttpURLConnection} with parameters. + * @param url + * @return an open connection + * @throws IOException + */ + private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException { + HttpURLConnection connection = createConnection(url); + + int timeoutMs = request.getTimeoutMs(); + connection.setConnectTimeout(timeoutMs); + connection.setReadTimeout(timeoutMs); + connection.setUseCaches(false); + connection.setDoInput(true); + + // use caller-provided custom SslSocketFactory, if any, for HTTPS + if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) { + ((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory); + } + + return connection; + } + + @SuppressWarnings("deprecation") + /* package */ static void setConnectionParametersForRequest(HttpURLConnection connection, + Request<?> request) throws IOException, AuthFailureError { + switch (request.getMethod()) { + case Method.DEPRECATED_GET_OR_POST: + // This is the deprecated way that needs to be handled for backwards compatibility. + // If the request's post body is null, then the assumption is that the request is + // GET. Otherwise, it is assumed that the request is a POST. + byte[] postBody = request.getPostBody(); + if (postBody != null) { + // Prepare output. There is no need to set Content-Length explicitly, + // since this is handled by HttpURLConnection using the size of the prepared + // output stream. + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.addRequestProperty(HEADER_CONTENT_TYPE, + request.getPostBodyContentType()); + DataOutputStream out = new DataOutputStream(connection.getOutputStream()); + out.write(postBody); + out.close(); + } + break; + case Method.GET: + // Not necessary to set the request method because connection defaults to GET but + // being explicit here. + connection.setRequestMethod("GET"); + break; + case Method.DELETE: + connection.setRequestMethod("DELETE"); + break; + case Method.POST: + connection.setRequestMethod("POST"); + addBodyIfExists(connection, request); + break; + case Method.PUT: + connection.setRequestMethod("PUT"); + addBodyIfExists(connection, request); + break; + default: + throw new IllegalStateException("Unknown method type."); + } + } + + private static void addBodyIfExists(HttpURLConnection connection, Request<?> request) + throws IOException, AuthFailureError { + byte[] body = request.getBody(); + if (body != null) { + connection.setDoOutput(true); + connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType()); + DataOutputStream out = new DataOutputStream(connection.getOutputStream()); + out.write(body); + out.close(); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/networking/WPTrustManager.java b/WordPress/src/main/java/org/wordpress/android/networking/WPTrustManager.java new file mode 100644 index 000000000..8f7bb9104 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/networking/WPTrustManager.java @@ -0,0 +1,119 @@ +package org.wordpress.android.networking; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.AppLog.T; + +public class WPTrustManager implements X509TrustManager { + private X509TrustManager defaultTrustManager; + private X509TrustManager localTrustManager; + private X509Certificate[] acceptedIssuers; + + public WPTrustManager(KeyStore localKeyStore) { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + + defaultTrustManager = findX509TrustManager(tmf); + if (defaultTrustManager == null) { + throw new IllegalStateException("Couldn't find X509TrustManager"); + } + + localTrustManager = new LocalStoreX509TrustManager(localKeyStore); + + List<X509Certificate> allIssuers = new ArrayList<X509Certificate>(); + Collections.addAll(allIssuers, defaultTrustManager.getAcceptedIssuers()); + Collections.addAll(allIssuers, localTrustManager.getAcceptedIssuers()); + acceptedIssuers = allIssuers.toArray(new X509Certificate[allIssuers.size()]); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + + private static X509TrustManager findX509TrustManager(TrustManagerFactory tmf) { + TrustManager tms[] = tmf.getTrustManagers(); + for (int i = 0; i < tms.length; i++) { + if (tms[i] instanceof X509TrustManager) { + return (X509TrustManager) tms[i]; + } + } + return null; + } + + + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + defaultTrustManager.checkClientTrusted(chain, authType); + } catch (CertificateException ce) { + localTrustManager.checkClientTrusted(chain, authType); + } + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + defaultTrustManager.checkServerTrusted(chain, authType); + } catch (CertificateException ce) { + localTrustManager.checkServerTrusted(chain, authType); + } + } + + public X509Certificate[] getAcceptedIssuers() { + return acceptedIssuers; + } + + static class LocalStoreX509TrustManager implements X509TrustManager { + private X509TrustManager trustManager; + + LocalStoreX509TrustManager(KeyStore localKeyStore) { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(localKeyStore); + + trustManager = findX509TrustManager(tmf); + if (trustManager == null) { + throw new IllegalStateException("Couldn't find X509TrustManager"); + } + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + trustManager.checkClientTrusted(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + trustManager.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + AppLog.e(T.API, "Cannot trust the certificate with the local trust manager...", e); + try { + SelfSignedSSLCertsManager.getInstance(null).setLastFailureChain(chain); + } catch (GeneralSecurityException e1) { + } catch (IOException e1) { + } + throw e; + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trustManager.getAcceptedIssuers(); + } + } +}
\ No newline at end of file |