-
-
Save 4sskick/d1318379500747564d8f1c8d6af1f796 to your computer and use it in GitHub Desktop.
OkHttp 3 implementation of Volley's HttpStack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2016 Eric Cochran | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.boardroomlimited.ignite.requests; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.os.Build; | |
import android.util.Log; | |
import android.widget.Toast; | |
import com.android.volley.AuthFailureError; | |
import com.android.volley.Header; | |
import com.android.volley.Request; | |
import com.android.volley.toolbox.BaseHttpStack; | |
import com.android.volley.toolbox.HttpResponse; | |
import com.boardroomlimited.ignite.activities.noconnection.NoConnectionActivity; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.security.KeyStore; | |
import java.security.KeyStoreException; | |
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.ArrayList; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.concurrent.TimeUnit; | |
import javax.net.ssl.SSLContext; | |
import javax.net.ssl.SSLSocket; | |
import javax.net.ssl.SSLSocketFactory; | |
import javax.net.ssl.TrustManager; | |
import javax.net.ssl.TrustManagerFactory; | |
import javax.net.ssl.X509TrustManager; | |
import okhttp3.Call; | |
import okhttp3.Headers; | |
import okhttp3.Interceptor; | |
import okhttp3.MediaType; | |
import okhttp3.OkHttpClient; | |
import okhttp3.RequestBody; | |
import okhttp3.Response; | |
import okhttp3.ResponseBody; | |
public class PinningOkClientBuilder extends BaseHttpStack { | |
private String[] mCertList = {"fake.com_public_certificate"}; | |
private final List<Interceptor> mInterceptors; | |
private final Context mContext; | |
public PinningOkClientBuilder(Context mContext, List<Interceptor> interceptors) { | |
this.mContext = mContext; | |
this.mInterceptors = interceptors; | |
} | |
private static void setConnectionParametersForRequest(okhttp3.Request.Builder builder, Request<?> request) | |
throws AuthFailureError { | |
switch (request.getMethod()) { | |
case Request.Method.DEPRECATED_GET_OR_POST: | |
// Ensure backwards compatibility. Volley assumes a request with a null body is a GET. | |
byte[] postBody = request.getBody(); | |
if (postBody != null) { | |
builder.post(RequestBody.create(MediaType.parse(request.getBodyContentType()), postBody)); | |
} | |
break; | |
case Request.Method.GET: | |
builder.get(); | |
break; | |
case Request.Method.DELETE: | |
builder.delete(createRequestBody(request)); | |
break; | |
case Request.Method.POST: | |
builder.post(createRequestBody(request)); | |
break; | |
case Request.Method.PUT: | |
builder.put(createRequestBody(request)); | |
break; | |
case Request.Method.HEAD: | |
builder.head(); | |
break; | |
case Request.Method.OPTIONS: | |
builder.method("OPTIONS", null); | |
break; | |
case Request.Method.TRACE: | |
builder.method("TRACE", null); | |
break; | |
case Request.Method.PATCH: | |
builder.patch(createRequestBody(request)); | |
break; | |
default: | |
throw new IllegalStateException("Unknown method type."); | |
} | |
} | |
private static RequestBody createRequestBody(Request r) throws AuthFailureError { | |
final byte[] body = r.getBody(); | |
if (body == null) { | |
return null; | |
} | |
return RequestBody.create(MediaType.parse(r.getBodyContentType()), body); | |
} | |
@Override | |
public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError { | |
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); | |
int timeoutMs = request.getTimeoutMs(); | |
clientBuilder.connectTimeout(timeoutMs, TimeUnit.MILLISECONDS); | |
clientBuilder.readTimeout(timeoutMs, TimeUnit.MILLISECONDS); | |
clientBuilder.writeTimeout(timeoutMs, TimeUnit.MILLISECONDS); | |
okhttp3.Request.Builder okHttpRequestBuilder = new okhttp3.Request.Builder(); | |
okHttpRequestBuilder.url(request.getUrl()); | |
Map<String, String> headers = request.getHeaders(); | |
for (final String name : headers.keySet()) { | |
okHttpRequestBuilder.addHeader(name, headers.get(name)); | |
} | |
for (final String name : additionalHeaders.keySet()) { | |
okHttpRequestBuilder.addHeader(name, additionalHeaders.get(name)); | |
} | |
setConnectionParametersForRequest(okHttpRequestBuilder, request); | |
//ssl init | |
try { | |
SSLContext sslContext = getSSLSocketFactory(); | |
if (sslContext != null) { | |
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); | |
trustManagerFactory.init((KeyStore) null); // Use null for default trust store | |
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) | |
clientBuilder.sslSocketFactory(sslContext.getSocketFactory() | |
, (X509TrustManager) getWrappedTrustManagers(trustManagers)[0]); | |
else clientBuilder.sslSocketFactory(sslContext.getSocketFactory()); | |
} | |
} catch (KeyStoreException | NoSuchAlgorithmException e) { | |
e.printStackTrace(); | |
Toast.makeText(mContext, "Failed to set SSL pinning", Toast.LENGTH_SHORT).show(); | |
} | |
for (Interceptor interceptor : mInterceptors) { | |
clientBuilder.addNetworkInterceptor(interceptor); | |
} | |
OkHttpClient client = clientBuilder.build(); | |
okhttp3.Request okHttpRequest = okHttpRequestBuilder.build(); | |
Call okHttpCall = client.newCall(okHttpRequest); | |
Response okHttpResponse = okHttpCall.execute(); | |
int code = okHttpResponse.code(); | |
ResponseBody body = okHttpResponse.body(); | |
InputStream content = body == null ? null : body.byteStream(); | |
int contentLength = body == null ? 0 : (int) body.contentLength(); | |
List<Header> responseHeaders = mapHeaders(okHttpResponse.headers()); | |
return new HttpResponse(code, responseHeaders, contentLength, content); | |
} | |
private List<Header> mapHeaders(Headers responseHeaders) { | |
List<Header> headers = new ArrayList<>(); | |
for (int i = 0, len = responseHeaders.size(); i < len; i++) { | |
final String name = responseHeaders.name(i), value = responseHeaders.value(i); | |
if (name != null) { | |
headers.add(new Header(name, value)); | |
} | |
} | |
return headers; | |
} | |
private SSLContext getSSLSocketFactory() { | |
try { | |
InputStream inputStream = mContext.getAssets().open(mCertList[0] + ".cer"); | |
CertificateFactory cf = CertificateFactory.getInstance("X.509"); | |
Certificate ca = cf.generateCertificate(inputStream); //already pinned the certificate | |
inputStream.close(); | |
//might no need using the keystore explicitly | |
//custom trust manager can directly reference the loaded certificate object from asset folder | |
KeyStore keyStore = KeyStore.getInstance("BKS"); | |
keyStore.load(null, null); | |
keyStore.setCertificateEntry(mCertList[0], ca); | |
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); | |
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); | |
tmf.init(keyStore); | |
TrustManager[] wrappedTrustManager = getWrappedTrustManagers(tmf.getTrustManagers()); | |
final SSLContext sslContext = SSLContext.getInstance("TLS"); | |
sslContext.init(null, wrappedTrustManager, null); | |
return sslContext; | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return null; | |
} | |
private TrustManager[] getWrappedTrustManagers(TrustManager[] trustManagers) { | |
final X509TrustManager originalTrustManager = (X509TrustManager) trustManagers[0]; | |
return new TrustManager[]{new X509TrustManager() { | |
@Override | |
public void checkClientTrusted(X509Certificate[] chain, String authType) { | |
// Not used for server certificate validation | |
} | |
@Override | |
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |
try { | |
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); | |
trustManagerFactory.init((KeyStore) null); // Use null for default trust store | |
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); | |
X509TrustManager defaultTrust = (X509TrustManager) trustManagers[0]; | |
defaultTrust.checkServerTrusted(chain, authType); | |
} catch (NoSuchAlgorithmException | KeyStoreException e) { | |
e.printStackTrace(); | |
// when system validation fails, check against pinned certificate | |
try { | |
if (!verifyChain(chain)) { | |
goingToNoConnection(); | |
Toast.makeText(mContext, "Invalid Certificate.", Toast.LENGTH_SHORT).show(); | |
} | |
} catch (IOException ex) { | |
ex.printStackTrace(); | |
} | |
} | |
} | |
private boolean verifyChain(X509Certificate[] chain) throws IOException { | |
// Check if the chain contains the pinned certificate | |
// validation relies on comparing the entire certificates themselves, not their hashes, | |
// no need the hash calculation or comparing | |
for (X509Certificate cert : chain) { | |
Log.e("ssl ok", "Certificate Subject: " + cert.getSubjectX500Principal()); | |
X509Certificate pinnedCert = getPinnedCertificate(); | |
if (pinnedCert != null && cert.equals(pinnedCert)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
@Override | |
public X509Certificate[] getAcceptedIssuers() { | |
// return new X509Certificate[0]; | |
return originalTrustManager.getAcceptedIssuers(); | |
} | |
}}; | |
} | |
private X509Certificate getPinnedCertificate() throws IOException { | |
try (InputStream in = mContext.getAssets().open(mCertList[0] + ".cer")) { | |
CertificateFactory cf = CertificateFactory.getInstance("X.509"); | |
return (X509Certificate) cf.generateCertificate(in); | |
} catch (CertificateException e) { | |
Log.e("ssl ok", "getPinnedCertificate: Error loading certificate", e); | |
return null; | |
} | |
} | |
private void goingToNoConnection() { | |
mContext.startActivity(new Intent(mContext, NoConnectionActivity.class)); | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.io.IOException; | |
import okhttp3.Headers; | |
import okhttp3.Interceptor; | |
import okhttp3.Request; | |
import okhttp3.Response; | |
import okhttp3.ResponseBody; | |
public class PinningOkClientInterceptor implements Interceptor { | |
@Override | |
public Response intercept(Chain chain) throws IOException { | |
Request request = chain.request(); | |
// Log Request Details | |
String requestUrl = request.url().toString(); | |
String requestMethod = request.method(); | |
Headers requestHeaders = request.headers(); | |
StringBuilder requestBodyBuilder = new StringBuilder(); | |
if (request.body() != null) { | |
requestBodyBuilder.append("Request Body: "); | |
requestBodyBuilder.append(request.body().toString()); | |
} | |
StringBuilder logMessage = new StringBuilder(); | |
logMessage.append("--> Sending request ") | |
.append(requestMethod) | |
.append(" ") | |
.append(requestUrl) | |
.append("\n"); | |
for (int i = 0; i < requestHeaders.size(); i++) { | |
logMessage.append("--> Header: ") | |
.append(requestHeaders.name(i)) | |
.append(": ") | |
.append(requestHeaders.value(i)) | |
.append("\n"); | |
} | |
logMessage.append(requestBodyBuilder.toString()) | |
.append("\n"); | |
// Send request and get response | |
Response response = chain.proceed(request); | |
// Log Response Details | |
int responseCode = response.code(); | |
Headers responseHeaders = response.headers(); | |
StringBuilder responseBodyBuilder = new StringBuilder(); | |
try (ResponseBody responseBody = response.peekBody(4096)) { | |
if (responseBody != null) { | |
responseBodyBuilder.append("Response Body: "); | |
responseBodyBuilder.append(responseBody.string()); | |
} | |
} | |
logMessage.append("<-- Received response: ") | |
.append(responseCode) | |
.append("\n"); | |
for (int i = 0; i < responseHeaders.size(); i++) { | |
logMessage.append("<-- Header: ") | |
.append(responseHeaders.name(i)) | |
.append(": ") | |
.append(responseHeaders.value(i)) | |
.append("\n"); | |
} | |
logMessage.append(responseBodyBuilder.toString()) | |
.append("\n"); | |
// Use your preferred logging library (e.g., Logcat) | |
System.out.println(logMessage.toString()); | |
return response; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
build.gradle (app under src)
implementation 'com.android.volley:volley:1.2.1'
implementation 'com.squareup.okhttp3:okhttp:3.9.0'
implementation 'com.squareup.okhttp:okhttp-urlconnection:1.6.0'
implement the request combine with Volley
RequestQueue requestQueue = Volley.newRequestQueue(mActivity, new PinningOkClientBuilder(mActivity, new ArrayList() {{
if (BuildConfig.DEBUG) add(new PinningOkClientInterceptor());
}}));
requestQueue.start();
and do build request as usual
Request apiRequest = new StringRequest(GET, requestURL, successListener, errorListener) { };