さくらインターネットが5月でTLS1.0/1.1が使えなくなるため、cocos2d-xで作ったゲームがAndroid v4.1からv4.4(API Level 16~19)において通信ができなくなる問題が起きました。
※SSLSocket参考
基本的には以下のところで紹介された方法でやりましたが、cocos2d-x周りに少し変更が必要でした。
Android 4.1+ enable TLS 1.1 and TLS 1.2
ソースコードはこちらです。
今回対応が必要としたcocos2d-xのバージョンは3.8.1です。
- まずは以下の内容でTLSSocketFactory.javaファイルを作ります。
package org.cocos2dx.lib; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; /** * @author fkrauthan */ public class TLSSocketFactory extends SSLSocketFactory { private SSLSocketFactory internalSSLSocketFactory; public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, null, null); internalSSLSocketFactory = context.getSocketFactory(); } public TLSSocketFactory(SSLSocketFactory sslSocketFactory) throws KeyManagementException, NoSuchAlgorithmException { internalSSLSocketFactory = sslSocketFactory; } @Override public String[] getDefaultCipherSuites() { return internalSSLSocketFactory.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return internalSSLSocketFactory.getSupportedCipherSuites(); } @Override public Socket createSocket() throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); } @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); } @Override public Socket createSocket(String host, int port) throws IOException, UnknownHostException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); } private Socket enableTLSOnSocket(Socket socket) { if(socket != null && (socket instanceof SSLSocket)) { ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"}); } return socket; } }
ここ変えたのは最初のパッケージ名と最後のTLSv1.1を消すぐらいです。さくらインターネットはTLSv1.1も無効にするので不要かと。
- 上記TLSSocketFactory.javaをcocos2dxプロジェクトルート/cocos2d/cocos/platform/android/java/src/org/cocos2dxに保存します。
- /cocos2d/cocos/platform/android/java/src/org/cocos2dx/lib/Cocos2dxHttpURLConnection.javaを変更します。
- 54行あたりに以下を追加
import org.cocos2dx.lib.TLSSocketFactory; import javax.net.ssl.SSLSocketFactory;
- 131行あたりに以下のをコメントアウト
SSLContext context = SSLContext.getInstance("TLS"); context.init(null, tmf.getTrustManagers(), null); httpsURLConnection.setSSLSocketFactory(context.getSocketFactory());
- 上記コメントアウトの箇所すぐ下に以下のを追加
SSLSocketFactory noSSLv3Factory = new TLSSocketFactory(); httpsURLConnection.setSSLSocketFactory(noSSLv3Factory);
- 54行あたりに以下を追加
- 本来ここまでやれば良いはずですが、cocosはssl認証のような面倒なことはやってくれませんでした。つまり、通常HTTPS通信行う時、上記変えた箇所の
setVerifySSL(HttpURLConnection urlConnection, String sslFilename)
メソッドには通っていないんです。
そのため、こちらのSSL証明書をチェックさせる方法で無理に通らせることにしました。
HttpRequest
を作る前に以下を追加しました。auto path = FileUtils::getInstance()->fullPathForFilename("cacert.pem"); HttpClient::getInstance()->setSSLVerification(path);
ちなみに、このcacert.pemはどこから来たかというと、自分サイトから以下のコマンド実行すると得られます。
openssl s_client -showcerts -connect ホスト名:443 </dev/null 2>/dev/null|openssl x509 -outform PEM >cacert.pem
そしてこのpemファイルをresディレクトリなどの場所に置き、Xcodeのプロジェクトにも追加すれば良いでしょう。
※How to save a remote server SSL certificate locally as a fileのelec3647の回答
余談になりますが、実際全く違うサイトの証明書を落としてきて試したところ、ここのca証明書なんでもいいようです。
/cocos2d/cocos/network/HttpClient-android.cppの541行あたりのsetVerifySSL()
を見ると分かりますが、まずはssl caファイルをチェックし、それがなければ狙いのsetVerifySSL
に通してくれないので、あまり意味がなくても設定します。
※あっ!もしかして、そのif(_client->getSSLVerification().empty()) return
をコメントアウトする手もあったでは?! - 最後に/cocos2d/cocos/network/HttpAsynConnection-apple.mの193行あたりに立て続け
[(id)certArrayRef release];
が2つありますので、そのうちの一つをコメントアウト(削除)します。
ここでもcocosのssl証明書に関するやる気のなさが窺えます。
これでAndroid v4.1~v4.4の端末でも問題なくHttpRequest
で通信できるはずです。
お気づきの方もいらっしゃると思いますが、Cocos2dxHttpURLConnection.javaのsetVerifySSL(HttpURLConnection urlConnection, String sslFilename)
メソッド中では、SSLContext
作る前に証明書やキーなどいろいろと準備してましたが、今回の変更で全く使われなくなります。
こちらのgotevが紹介した方法でやりたくなります(実際やりたくなりました^^;)が、下記のエラーが出てしまいます。
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
ここは深入りしてませんが、つまりtmf.getTrustManagers()
を渡してcontext.init(null, tmf.getTrustManagers(), null);
するとパスが見つからないというエラーが出ますが、context.init(null, null, null);
にするといいです。
この問題はstevieが言ってることがを参考にするといいかもしれません。