/* * Copyright (C) 2015 Actor LLC. <https://actor.im> */ package im.actor.runtime.android; import android.content.res.Resources; import com.squareup.okhttp.Callback; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.util.Collection; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManagerFactory; import im.actor.runtime.HttpRuntime; import im.actor.runtime.Log; import im.actor.runtime.http.HTTPError; import im.actor.runtime.http.HTTPResponse; import im.actor.runtime.promise.Promise; import okio.Buffer; public class AndroidHttpProvider implements HttpRuntime { private static final String TAG = "AndroidHTTP"; private final OkHttpClient client = new OkHttpClient(); private final MediaType MEDIA_TYPE = MediaType.parse("application/octet-stream"); public AndroidHttpProvider() { Resources resources = AndroidContext.getContext().getResources(); try { String cert = resources.getString(resources.getIdentifier("trusted_pem", "string", AndroidContext.getContext().getPackageName())); SSLContext sslContext = sslContextForTrustedCertificates(new Buffer() .writeUtf8(cert) .inputStream()); client.setSslSocketFactory(sslContext.getSocketFactory()); } catch (Resources.NotFoundException e) { // Just Ignore } try { final String trustHostname = resources.getString(resources.getIdentifier("trusted_hostname", "string", AndroidContext.getContext().getPackageName())); client.setHostnameVerifier((hostname, session) -> hostname.equals(trustHostname)); } catch (Resources.NotFoundException e) { // Just Ignore } } public SSLContext sslContextForTrustedCertificates(InputStream in) { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in); if (certificates.isEmpty()) { throw new IllegalArgumentException("expected non-empty set of trusted certificates"); } // Put the certificates a key store. char[] password = "password".toCharArray(); // Any password will work. KeyStore keyStore = newEmptyKeyStore(password); int index = 0; for (Certificate certificate : certificates) { String certificateAlias = Integer.toString(index++); keyStore.setCertificateEntry(certificateAlias, certificate); } // Wrap it up in an SSL context. KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, password); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); return sslContext; } catch (GeneralSecurityException e) { throw new RuntimeException(e); } } private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException { try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); InputStream in = null; // By convention, 'null' creates an empty key store. keyStore.load(in, password); return keyStore; } catch (IOException e) { throw new AssertionError(e); } } @Override public Promise<HTTPResponse> getMethod(String url, int startOffset, int size, int totalSize) { return new Promise<>(resolver -> { final Request request = new Request.Builder() .url(url) .addHeader("Range", "bytes=" + startOffset + "-" + (startOffset + size)) .build(); Log.d(TAG, "Downloading part: " + request.toString()); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { Log.d(TAG, "Downloading part error: " + request.toString()); e.printStackTrace(); // TODO: Better error? resolver.error(new HTTPError(0)); } @Override public void onResponse(Response response) throws IOException { Log.d(TAG, "Downloading part response: " + request.toString() + " -> " + response.toString()); if (response.code() >= 200 && response.code() < 300) { resolver.result(new HTTPResponse(response.code(), response.body().bytes())); } else { resolver.error(new HTTPError(response.code())); } } }); }); } @Override public Promise<HTTPResponse> putMethod(String url, byte[] contents) { return new Promise<>(resolver -> { final Request request = new Request.Builder() .url(url) .method("PUT", RequestBody.create(MEDIA_TYPE, contents)) .build(); Log.d(TAG, "Uploading part: " + request.toString()); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { Log.d(TAG, "Uploading part error: " + request.toString()); e.printStackTrace(); // TODO: Better error? resolver.error(new HTTPError(0)); } @Override public void onResponse(Response response) throws IOException { Log.d(TAG, "Upload part response: " + request.toString() + " -> " + response.toString()); if (response.code() >= 200 && response.code() < 300) { resolver.result(new HTTPResponse(response.code(), null)); } else { resolver.error(new HTTPError(response.code())); } } }); }); } }