OkHttpでオレオレ認証を通過する
railsなどにAPIサーバとしての機能をもたせたならば、そのAPIを叩く側、つまりandroid側でもオレオレ認証を通過できるようにしたい。デフォルトのandroidのHTTP通信をするDefaultHttpClientなどは分からないけど、今主流のHTTPクライアントなOkHttpなら割と楽にできる。
OkHttpはそれ単体としてコールバック機能をもったHTTPクライアントとしても利用できるし、また最近話題になっているRESTAPIを楽に叩けるインターフェースを備えたretrofitの中でもHTTPクライアントとして利用できる。もちろん以下に書くコードを追加してもretrofitはそれまでどおりに使える。オレオレ認証を通過するという機能を加えて。
実装
public class Customtrust { private static final OkHttpClient client = new OkHttpClient(); public Customtrust(Context context){ client.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); // 証明書の追加 // サーバ側の開発用オレオレ証明書のcrtを直接文字列に書きだしてそれを追加 SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream()); client.setSslSocketFactory(sslContext.getSocketFactory()); } public void execute(final Request request, final Callback callback){ client.newCall(request).enqueue(callback); } public OkHttpClient getClient(){ return client; } public static OkHttpClient createNewClient(){ OkHttpClient c = new OkHttpClient(); c.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { return true; } }); SSLContext sslContext = sslContextForTrustedCertificates(trustedCertificatesInputStream()); c.setSslSocketFactory(sslContext.getSocketFactory()); return c; } private static InputStream trustedCertificatesInputStream() { // PEM files for root certificates of Comodo and Entrust. These two CAs are sufficient to view // https://publicobject.com (Comodo) and https://squareup.com (Entrust). But they aren't // sufficient to connect to most HTTPS sites including https://godaddy.com and https://visa.com. // Typically developers will need to get a PEM file from their organization's TLS administrator. String developmentCertificationAuthority = "" + "-----BEGIN CERTIFICATE-----\n" + "MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\n" + "hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\n" + "A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\n" + "BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\n" + "MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\n" + "EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\n" + "Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\n" + "dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n" + "6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\n" + "pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n" + "9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n" + "/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\n" + "Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n" + "+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\n" + "qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\n" + "SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\n" + "u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\n" + "Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\n" + "crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\n" + "FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n" + "/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\n" + "wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n" + "4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n" + "2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\n" + "FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\n" + "CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\n" + "boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\n" + "jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\n" + "S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\n" + "QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n" + "0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\n" + "NVOFBkpdn627G190\n" + "-----END CERTIFICATE-----\n"; return new Buffer() .writeUtf8(developmentCertificationAuthority) .inputStream(); } private static 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 static 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); } } }
trustedCertificatesInputStreamというprivateメソッドの中で宣言している長い文字列はSSL通信に必要なサーバ証明書。以下の記事などに書いてあるコマンドによって生成されたサーバ証明書の内容をそのまま貼り付ければOK。
あとはこのOkHttpインスタンスをget出来るようなラッパーを作ればいい。そのラッパーもdaggerなどのDIを使って環境ごとに変更すればより良い。
OkHttpでオレオレ認証通過できるようにする日本語情報が無いなぁ…と色々探した末stackoverflowでこれに関する記事を見つけて喜んだものの、実はOkHttpのgithub内にあるレシピ集にそれが含まれてた。灯台下暗し。
android studioのエミュレータ(ADB)でSDカードを挿す
その昔、eclipseなどのプラグインとしてandroid開発するときについてきたAVD(Android Virtual Device、仮想デバイス、エミュレータ)は死ぬほど起動が遅かった。とんでもなく遅かった。エミュレータの起動が遅すぎて、1回エミュレータを起動している間にアプリを作り上げることができちゃったよHAHAHAなどというジョークも出てきたり出てこなかったりするほどだった。いやまぁ僕がたった今考えたジョークだから出てこなかったんだろうけど。それからGenyMotionというエミュレータが出てきた。virtualboxを利用してエミュレータを起動するのである。それまでのエミュレータに比べると爆速というレベルだった。
そして今、android studioが普及して、AVD周りのサポートがよくなった。GenyMotionを入れなくとも、そこそこ速いエミュレータを利用することが出来る。今後android studio側でスピードアップを含めたエミュレータ周りの改善がなされていくだろう、と考えているので、今はGenyMotionはつかわずandroid studioのAVDマネージャーを通して純正のエミュレータを利用している。
環境: mac OSX10.9
android studio: 1.4.1
で、その中で出会ったエラーというか変な現象。ギャラリーから画像を読み込みたいときは外部ストレージのパーミッションが必要になることがある。アプリ側でパーミッションの許可ダイアログを出して、設定からアプリのパーミッションをいじっても外部ストレージ、つまりはSDカードが読み込まれない。どういうわけか、SDカード自体がAVD側で認識されていないっぽいAVDマネージャーでSDカードを読み込むように設定しても、うまくいかない。 後述のstackoverflowの質問者はこういう感じで挿さってるかどうか確認してる。
//This prints: External: removed Log.d(TAG, "External: " + Environment.getExternalStorageState() );
で、色々探してたらstackoverflowでそれに関することが出てきた。どうやら手動でSDカードを挿さないといけないらしい。手動で、と言ってもAVDマネージャ経由でなくコンフィグを自分で書き換えるという意味。
以下のディレクトリにADVのコンフィグファイルが配置されている。
~/.android/avd/Nexus_5_API_23_x86.avd/config.ini
Nexus5...の部分のディレクトリ名は自分でつくったAVDの名前になるので変わることがある。で、とにかくこのconfig.iniの中にsdカードに関する以下の行が含まれているので、それをyesに書き換える。
hw.sdCard=yes
で、これでADVを再起動すればOK。無事にSDカードが認識された状態になる。 参考:
OkHttpで生のjsonを出力する
OkHttpというか、retrofitの話。retrofitとそのHTTPクライアントとしてOkHttpを使っているという前提。OkHttp単体でもいけるけど。
retrofitを使ってサーバ側とjsonでやり取りをしていると、retrofitで生のjsonレスポンスを見たくても見れない。基本retrofitではコールバック内で取得できるレスポンスはjsonコンバータによってjavaのオブジェクトに変換されているために見れない。見る方法もあるのかもしれないけど探した限りはそれっぽい情報は見つからなかったし、あったとしても今のところはそう簡単に見れないっぽい。
開発中やデバッグ時は生のjsonがどう出てるのか知りたいことが結構あると思うんだけど、それが簡易に出来ないのですごく不便。なのでそこをOkHttpのInterceptorを使って解決する。retrofit側で生のresponsebodyを出すよりもretrofitの中のHTTPクライアントの方で解決するほうが早かった。というかそれしか方法がわからなかった。
実装
public class RawJsonInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = chain.proceed(request); String rawJson = response.body().string(); Log.d("raw json", String.format("raw JSON response is: %s", rawJson)); return response.newBuilder().body(ResponseBody.create(response.body().contentType(), rawJson)).build(); } }
こういう感じのInterceptorを作る。特に難しいことはしていない。ただ、最後returnをしている部分でresponseを再作成しているのは一度しかbodyを読み込めないというOkHttpの仕様があるため。
あとはこれをOkHttpに噛ませればいい。
OkHttpClient client = new OkHttpClient(); client.interceptors().add(new RawJsonInterceptor());
OkHttpを使う
androidのhttpクライアントはデフォルトのを使う人はあんまり居ないんじゃないかと思う。 androidのhttpクライアントというとvolleyかokhttpの二択という感じがあるが、最近ではOkHttpのほうが動きが活発なのでそちらを選択する人が多いように見えるのでこっちに乗っかってみる。
インストール
android studioを使っているのなら、build.gradleに以下を突っ込む。
compile 'com.squareup.okhttp:okhttp:2.7.0'
かProjectStructureからLibraryDependencyで「okhttp」と検索してcom.squareupのものを追加する。
基本的な使い方
OkHttpClient client = new OkHttpClient(); public String run(String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); Response response = client.newCall(request).execute(); return response.body().string(); }
同期的な通信の手順としては
という感じ。
上のコードはOkHttpの公式サイトから引っ張ってきたものだけど、多分これと同じで実際に使うときはラッパーを用意してそいつに通信先のURLとか渡すことになると思う。
コールバックを使う
といっても同期通信よりもコールバックを用いた非同期通信をすることのほうが多いだろう。コールバックを用いた非同期通信は以下のようにする。
private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { e.printStackTrace(); } @Override public void onResponse(Response response) throws IOException { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Headers responseHeaders = response.headers(); for (int i = 0, size = responseHeaders.size(); i < size; i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } System.out.println(response.body().string()); } }); }
- OkHttpClientインスタンスの作成
- Requestインスタンスの作成
- コールバックオブジェクトの作成
- enqueueにコールバックを入れて通信開始
- 通信終了後にコールバック(onFailure または onResponse)呼び出し
上のコードではコールバックをそのままenqueueに突っ込んでるので3と4が同時だけど、当然コールバックを先に作ってからenqueueに渡すということもできる。なのでこれも
public void runAsync(String url, Callback callback)
みたいなラッパーを作って使うことが多くなるんじゃないかと思う。
onFailureとonResponse
コールバックの直接の処理となるonFailureとonResponseだが、400系や500系のレスポンスが返ってきた時はコールバックとしてonFailureが呼ばれるんじゃないかと思っていたのだけど、じつは違う。 onFailureはサーバ側までそもそもリクエストが届かなかった時などに呼び出される。 なので400系や500系も一応通信相手のサーバまで届いたものの「ダメでした」というレスポンスをもらったとしてカウントされるため、それらの場合も全部onResponseで処理することになる。 なのでonResponseのなかでresponse.code()でレスポンスコードを見て色々処理を出し分けたり、200系とそうでないものの区別が付けばいいだけなら上のコードのようにresponse.isSuccessful()でざっくり判断すればいい。
POSTリクエスト
もちろんPOSTも出来る。
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { String postBody = "" + "Releases\n" + "--------\n" + "\n" + " * _1.0_ May 6, 2013\n" + " * _1.1_ June 15, 2013\n" + " * _1.2_ August 11, 2013\n"; Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody)) .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); }
GETの時とほとんど同じ。Requestインスタンスを作成する際にpost()メソッドにリクエストボディを指定してあげればいい。リクエストボディはリクエストボディそのものとメディアタイプを指定することで作成。
POSTでフォームとか画像送ったりヘッダー指定したりしたいんだけど
基本は公式のgithub内にあるレシピ集を見ればだいたいなにが出来るか、どうやるかはわかると思う。
リクエストの前後にいろいろな処理を噛ませたい
例えばリクエストの際には基本的にあるヘッダーを付けたいとかって場合、
Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build();
というようにしてヘッダーをつけることもできるけど、OkHttpClientインスタンスにその役割を任せることも出来る。interceptorという機能を使う。
class LoggingInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); logger.info(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); logger.info(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); return response; } } OkHttpClient client = new OkHttpClient(); client.networkInterceptors().add(new LoggingInterceptor());
という感じでInterceptorインターフェースの実装クラスを作ってOkHttpClientインスタンスにそのインスタンスを持たせることで処理を噛ませることができる。この場合はリクエストした時刻とレスポンスの返ってきた時刻をログに書き出すということをしている。 chain.request()で取得しているのがリクエスト時に作ったRequestインスタンス、chain.proceed(request)で取得しているのがコールバックのonFailureやonResponseに渡る直前のResponseインスタンス。
NetworkInterceptorとApplicationInterceptor
interceptorにも2種類ある。それがNetworkInterceptorとApplicationInterceptor。
OkHttpClient client = new OkHttpClient(); // NetworkInterceptor client.networkInterceptors().add(new LoggingInterceptor()); // ApplicationInterceptor client.interceptors().add(new LoggingInterceptor());
指定の仕方はほとんど違わない。何が違うのかというと処理の回数が違う。 ApplicationInterceptorは1回のリクエストあたり1度きりしか処理をしない。NetworkInterceptorは1回のリクエストあたり複数回処理をすることがある。 というのは例えばリクエスト先Aが300系を返してBにリダイレクトを促した場合、リダイレクト先Bにもう1度リクエストを行うことになる。 なのでこの場合は2回リクエストが行われているのだけど、ApplicationInterceptorの場合はその場合でも1度しか呼ばれない。NetworkInterceptorの場合は2度呼ばれることになる。
なのでローレベルで色々処理を噛ませたいのならばNetworkInterceptorを、そうでないのならばApplicationInteceptorを使うことになる。ApplicationInterceptorがあれば十分な場合のが多いのではないだろうか。
というようなことが以下に丸々書いてある。
ちなみに
OkHttpClient client = new OkHttpClient();
client.interceptors();
などinterceptors()メソッドやnetworkInterceptors()で取得できるのはList
今はOkHttpをそのまま使うことはなくRestAPIインターフェースライブラリのRetrofitのhttpクライアントとして使っている。 なのでexecute()とかをそのままいじることは殆ど無いけど、InterceptorまわりはRetorfitを使っていてもいじることは結構あるのでinterceptor周りは覚えておいて損はないと思う。