/* HttpDConnectSDK.java Copyright (c) 2016 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.message; import android.net.Uri; import android.os.Build; import android.util.Log; import org.deviceconnect.message.entity.BinaryEntity; import org.deviceconnect.message.entity.Entity; import org.deviceconnect.message.entity.FileEntity; import org.deviceconnect.message.entity.MultipartEntity; import org.deviceconnect.message.entity.StringEntity; import org.deviceconnect.sdk.BuildConfig; import org.json.JSONException; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Map; import java.util.Random; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * HTTP通信を使用してDevice Connect Managerと通信を行うSDKクラス. * @author NTT DOCOMO, INC. */ class HttpDConnectSDK extends DConnectSDK { /** * デバック用フラグ. */ private static final boolean DEBUG = BuildConfig.DEBUG; /** * デバック用タグを定義します. */ private static final String TAG = "DConnectSDK"; /** * バッファサイズを定義します. */ private static final int BUF_SIZE = 4096; /** * 成功となるレスポンスコードを定義します. */ private static final int SUCCESS_RESPONSE_CODE = 200; /** * 接続のタイムアウト(ms). */ private static final int CONNECT_TIMEOUT = 30 * 1000; /** * 読み込みのタイムアウト時間(ms). */ private static final int READ_TIMEOUT = 3 * 60 * 1000; /** * WebSocketと接続を行うクラス. */ private DConnectWebSocketClient mWebSocketClient = new DConnectWebSocketClient(); /** * マルチパートのバウンダリーに付加するハイフンを定義. */ private final static String TWO_HYPHEN = "--"; /** * マルチパートの改行コードを定義. */ private final static String EOL = "\r\n"; /** * {@link DConnectSDKFactory}で生成させるためにpackageスコープにしておく。 */ HttpDConnectSDK() { } /** * 勝手サーバ証明書を許諾するHttpsURLConnectionを生成する. * * @param url 接続先のURL * @return HttpsURLConnectionのインスタンス * @throws IOException HttpsURLConnectionの生成に失敗した場合に発生 * @throws NoSuchAlgorithmException SSLの暗号化に失敗した場合に発生 * @throws KeyManagementException Keyの管理に失敗した場合の発生 */ private HttpsURLConnection makeHttpsURLConnection(final URL url) throws IOException, NoSuchAlgorithmException, KeyManagementException { HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); connection.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(final String hostname, final SSLSession sslSession) { return true; } }); TrustManager[] transManagers = { new X509TrustManager() { @Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { } @Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } }; SSLContext sslcontext = SSLContext.getInstance("SSL"); sslcontext.init(null, transManagers, new SecureRandom()); connection.setSSLSocketFactory(sslcontext.getSocketFactory()); return connection; } /** * コンテンツのサイズを計算する. * @param dataMap データ一覧 * @param boundary バウンダリ * @return コンテンツサイズ * @throws IOException サイズの計算に失敗した場合に発生 */ private int calcContentLength(final Map<String, Entity> dataMap, final String boundary) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); int size = writeMultipart(out, dataMap, boundary, false); return out.size() + size; } /** * マルチパートを指定したストリームに書き込む. * <p> * writeFlagがfalseの場合には、コンテンツのデータはストリームに書き込まない。<br> * Content-Lengthのサイズを算出する時に使用します。 * </p> * @param out マルチパートを書き込むストリーム * @param dataMap マルチパートに書き込むデータ * @param boundary マルチパートのバウンダリー * @param writeFlag コンテンツデータを書き込むフラグ * @return コンテンツデータサイズ * @throws IOException ファイルのオープンやストリームの書き込みに失敗した場合に発生 */ private int writeMultipart(final OutputStream out, final Map<String, Entity> dataMap, final String boundary, final boolean writeFlag) throws IOException { int contentLength = 0; BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")); for (Map.Entry<String, Entity> data : dataMap.entrySet()) { String key = data.getKey(); Entity val = data.getValue(); if (val instanceof StringEntity) { writer.write(String.format("%s%s%s", TWO_HYPHEN, boundary, EOL)); writer.write(String.format("Content-Disposition: form-data; name=\"%s\"%s", key, EOL)); writer.write(EOL); writer.write(((StringEntity) val).getContent()); writer.write(EOL); } else if (val instanceof BinaryEntity) { contentLength += (((BinaryEntity) val).getContent()).length; writer.write(String.format("%s%s%s", TWO_HYPHEN, boundary, EOL)); writer.write(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"%s", key, ((BinaryEntity) val).getName(), EOL)); writer.write(String.format("Content-Type: application/octet-stream%s", EOL)); writer.write(String.format("Content-Transfer-Encoding: binary%s", EOL)); writer.write(EOL); writer.flush(); if (writeFlag) { out.write(((BinaryEntity) val).getContent()); } writer.write(EOL); } else if (val instanceof FileEntity) { File file = ((FileEntity) val).getContent(); contentLength += file.length(); writer.write(String.format("%s%s%s", TWO_HYPHEN, boundary, EOL)); writer.write(String.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"%s", key, file.getName(), EOL)); writer.write(String.format("Content-Type: application/octet-stream%s", EOL)); writer.write(String.format("Content-Transfer-Encoding: binary%s", EOL)); writer.write(EOL); writer.flush(); if (writeFlag) { writeFile(out, file); } writer.write(EOL); } else { throw new IllegalArgumentException("data is not String or File. key=" + key + " value=" + val); } writer.flush(); } writer.write(String.format("%s%s%s%s", TWO_HYPHEN, boundary, TWO_HYPHEN, EOL)); writer.flush(); return contentLength; } /** * 指定されたストリームにファイルデータを書き込む. * <p> * 指定されたファイルが見つからない場合にはIOExceptionを発生する。 * </p> * @param out データを書き込むストリーム * @param file 書き込みファイル * @throws IOException 読み込むファイルが見つからない場合に発生 */ private void writeFile(final OutputStream out, final File file) throws IOException { byte[] buf = new byte[4096]; int len; FileInputStream fis = null; try { fis = new FileInputStream(file); while ((len = fis.read(buf)) > 0) { out.write(buf, 0, len); out.flush(); } } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 指定したURIに接続を行い通信結果を返却する. * * @param method HTTPメソッド * @param uri 通信先のURI * @param headers HTTPリクエストに追加するヘッダーリスト(ヘッダーを追加しない場合にはnull) * @param body HTTPリクエストに追加するボディデータ(ボディを追加しない場合にはnull) * @return 通信結果 * @throws IOException HttpsURLConnectionの生成に失敗した場合に発生 * @throws NoSuchAlgorithmException SSLの暗号化に失敗した場合に発生 * @throws KeyManagementException Keyの管理に失敗した場合の発生 */ private byte[] connect(final Method method, final String uri, final Map<String, String> headers, final Entity body) throws IOException, NoSuchAlgorithmException, KeyManagementException { if (DEBUG) { Log.d(TAG, "connect: method=" + method + " uri=" + uri); if (headers != null) { for (String key : headers.keySet()) { Log.d(TAG, "header: " + key + "=" + headers.get(key)); } } if (body != null) { Log.d(TAG, "body: " + body); } } String boundary = String.format("%x", new Random().hashCode()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); HttpURLConnection conn = null; try { if (uri.startsWith("https://")) { conn = makeHttpsURLConnection(new URL(uri)); } else { conn = (HttpURLConnection) new URL(uri).openConnection(); } conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); conn.setRequestMethod(method.getValue()); conn.setDoInput(true); conn.setDoOutput(Method.POST.equals(method) || Method.PUT.equals(method)); if (headers != null) { for (String key : headers.keySet()) { conn.setRequestProperty(key, headers.get(key)); } } // GotAPI 1.1からヘッダーにオリジンが必須になったので、ここで追加を行う // 参考: http://technical.openmobilealliance.org/Technical/technical-information/release-program/current-releases/generic-open-terminal-api-framework-1-1 if (getOrigin() != null) { conn.setRequestProperty(DConnectMessage.HEADER_GOTAPI_ORIGIN, getOrigin()); } // 4.x系はkeep-aliveを行うと例外が発生するため、offにする // 参考: http://osa030.hatenablog.com/entry/2015/05/22/181155 if (Build.VERSION.SDK_INT > 13 && Build.VERSION.SDK_INT < 19) { conn.setRequestProperty("Connection", "close"); } // マルチパートのContentTypeを設定する if (body != null && body instanceof MultipartEntity) { conn.setRequestProperty("Content-Type", String.format("multipart/form-data; boundary=%s", boundary)); conn.setFixedLengthStreamingMode(calcContentLength(((MultipartEntity) body).getContent(), boundary)); } conn.setUseCaches(false); conn.connect(); // Bodyにデータが存在する場合には、データを書き込む if (body != null && (Method.POST.equals(method) || Method.PUT.equals(method))) { OutputStream os = conn.getOutputStream(); if (body instanceof BinaryEntity) { os.write(((BinaryEntity) body).getContent()); } else if (body instanceof StringEntity) { os.write(((StringEntity) body).getContent().getBytes()); } else if (body instanceof FileEntity) { writeFile(os, ((FileEntity) body).getContent()); } else if (body instanceof MultipartEntity) { writeMultipart(os, ((MultipartEntity) body).getContent(), boundary, true); } os.flush(); os.close(); } int resp = conn.getResponseCode(); if (resp == SUCCESS_RESPONSE_CODE) { InputStream in = conn.getInputStream(); int len; byte[] buf = new byte[BUF_SIZE]; while ((len = in.read(buf)) > 0) { baos.write(buf, 0, len); } in.close(); } else { if (DEBUG) { Log.w(TAG, "Failed to connect the server. response=" + resp); } InputStream in = conn.getErrorStream(); int len; byte[] buf = new byte[BUF_SIZE]; while ((len = in.read(buf)) > 0) { baos.write(buf, 0, len); } in.close(); } } finally { if (conn != null) { conn.disconnect(); } } return baos.toByteArray(); } /** * 指定されたデータからDConnectResponseMessageを生成する. * @param result 通信結果のデータ * @return DConnectResponseMessageのインスタンス */ private DConnectResponseMessage createMessage(final byte[] result) { if (result == null) { return new DConnectResponseMessage(DConnectMessage.ErrorCode.ACCESS_FAILED); } try { return new DConnectResponseMessage(new String(result, "UTF-8")); } catch (JSONException e) { return createErrorMessage(DConnectMessage.ErrorCode.UNKNOWN.getCode(), e.getMessage()); } catch (UnsupportedEncodingException e) { return createErrorMessage(DConnectMessage.ErrorCode.UNKNOWN.getCode(), e.getMessage()); } } @Override protected DConnectResponseMessage sendRequest(final Method method, final Uri uri, final Map<String, String> headers, final Entity body) { if (method == null) { throw new NullPointerException("method is null."); } if (uri == null) { throw new NullPointerException("uri is null."); } try { return createMessage(connect(method, uri.toString(), headers, body)); } catch (MalformedURLException e) { throw new IllegalArgumentException("uri is invalid."); } catch (SocketTimeoutException e) { return createTimeoutResponse(); } catch (Exception e) { return createErrorMessage(DConnectMessage.ErrorCode.UNKNOWN.getCode(), e.getMessage()); } catch (OutOfMemoryError e) { return createErrorMessage(DConnectMessage.ErrorCode.UNKNOWN.getCode(), e.getMessage()); } } @Override public void connectWebSocket(final OnWebSocketListener listener) { if (listener == null) { throw new NullPointerException("listener is null."); } URIBuilder builder = createURIBuilder(); builder.setScheme(isSSL() ? "wss" : "ws"); builder.setPath("/gotapi/websocket"); mWebSocketClient.setOnWebSocketListener(listener); mWebSocketClient.connect(builder.toString(), getOrigin(), getAccessToken()); } @Override public void disconnectWebSocket() { mWebSocketClient.close(); } @Override public boolean isConnectedWebSocket() { return mWebSocketClient.isConnected(); } @Override public void addEventListener(final Uri uri, final OnEventListener listener) { if (uri == null) { throw new NullPointerException("uri is null."); } if (listener == null) { throw new NullPointerException("listener is null."); } put(uri, null, new OnResponseListener() { @Override public void onResponse(final DConnectResponseMessage response) { if (response.getResult() == DConnectMessage.RESULT_OK) { mWebSocketClient.addEventListener(uri, listener); } listener.onResponse(response); } }); } @Override public void removeEventListener(final Uri uri) { if (uri == null) { throw new NullPointerException("uri is null."); } delete(uri, new OnResponseListener() { @Override public void onResponse(final DConnectResponseMessage response) { } }); if (mWebSocketClient != null) { mWebSocketClient.removeEventListener(uri); } } }