| 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(); |
| } |
| } |
| } |