/* HttpUtil.java Copyright (c) 2017 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.test.http; import android.os.Build; import android.util.Log; import org.deviceconnect.android.manager.test.BuildConfig; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; 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通信を行うためのユーティリティクラス. */ public final class HttpUtil { /** * デバック用フラグ. */ private static final boolean DEBUG = BuildConfig.DEBUG; /** * デバック用タグを定義します. */ private static final String TAG = "DConnect"; /** * バッファサイズを定義します. */ private static final int BUF_SIZE = 4096; /** * エラーとなるレスポンスコードを定義します. */ private static final int ERROR_RESPONSE_CODE = 400; /** * 接続のタイムアウト. */ private static final int CONNECT_TIMEOUT = 10 * 1000; /** * 読み込みのタイムアウト時間. */ private static final int READ_TIMEOUT = 3 * 60 * 1000; /** * POSTメソッドを定義します. */ private static final String METHOD_POST = "POST"; /** * PUTメソッドを定義します. */ private static final String METHOD_PUT = "PUT"; /** * GETメソッドを定義します. */ private static final String METHOD_GET = "GET"; /** * DELETEメソッドを定義します. */ private static final String METHOD_DELETE = "DELETE"; /** * マルチパートのバウンダリーに付加するハイフンを定義. */ private final static String TWO_HYPHEN = "--"; /** * マルチパートの改行コードを定義. */ private final static String EOL = "\r\n"; /** * コンストラクタ. * ユーティリティクラスなので、インスタンスは作成させない。 */ private HttpUtil() { } /** * SSLに対応したHttpsURLConnectionを生成する. * @param url 接続先のURL * @return HttpsURLConnectionのインスタンス。 * @throws IOException 生成に失敗した場合に発生 * @throws NoSuchAlgorithmException SSLを生成するためのアルゴリズムが見つからない場合に発生 * @throws KeyManagementException SSLのキーの管理に失敗した場合に発生 */ private static 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; } /** * マルチパートを指定したストリームに書き込む. * <p> * writeFlagがfalseの場合には、コンテンツのデータはストリームに書き込まない。<br> * Content-Lengthのサイズを算出する時に使用します。 * </p> * @param out マルチパートを書き込むストリーム * @param dataMap マルチパートに書き込むデータ * @param boundary マルチパートのバウンダリー * @param writeFlag コンテンツデータを書き込むフラグ * @return コンテンツデータサイズ * @throws IOException ファイルのオープンやストリームの書き込みに失敗した場合に発生 */ private static int writeMultipart(final OutputStream out, final Map<String, Object> 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, Object> data : dataMap.entrySet()) { String key = data.getKey(); Object val = data.getValue(); if (val instanceof String) { 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((String) val); writer.write(EOL); } else if (val instanceof byte[]) { contentLength += ((byte[]) val).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, "byte", 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((byte[]) val); } writer.write(EOL); } else if (val instanceof File) { File file = (File) val; 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; } /** * コンテンツのサイズを計算する. * @param dataMap データ一覧 * @param boundary バウンダリ * @return コンテンツサイズ * @throws IOException サイズの計算に失敗した場合に発生 */ private static int calcContentLength(final Map<String, Object> dataMap, final String boundary) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); int size = writeMultipart(out, dataMap, boundary, false); return out.size() + size; } /** * 指定されたストリームにファイルデータを書き込む. * <p> * 指定されたファイルが見つからない場合にはIOExceptionを発生する。 * </p> * @param out データを書き込むストリーム * @param file 書き込みファイル * @throws IOException 読み込むファイルが見つからない場合に発生 */ private static 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(); } } } } /** * 指定されたサーバと通信を行い結果をresponseに格納する. * @param response レスポンスを格納するクラス * @param method HTTPメソッド * @param uri 接続先のURI * @param headers HTTPヘッダー * @param body HTTPボディ * @param invalidDataFlag 不正なデータを作成するフラグ */ private static void connect(final Response response, final String method, final String uri, final Map<String, String> headers, final Object body, final boolean invalidDataFlag) { 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()); 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); 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)); } } // 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 Map) { if (invalidDataFlag) { // invalidDataがtrueの場合にはmultipartのデータの一部を不正なデータにする conn.setRequestProperty("Content-Type", String.format("multipart/form-data")); conn.setFixedLengthStreamingMode(calcContentLength((Map) body, boundary)); } else { conn.setRequestProperty("Content-Type", String.format("multipart/form-data; boundary=%s", boundary)); conn.setFixedLengthStreamingMode(calcContentLength((Map) body, boundary)); } } // キャッシュをOFF conn.setUseCaches(false); conn.connect(); if (body != null && (METHOD_POST.equals(method) || METHOD_PUT.equals(method))) { OutputStream os = conn.getOutputStream(); if (body instanceof byte[]) { os.write((byte[]) body); } else if (body instanceof String) { os.write(((String) body).getBytes()); } else if (body instanceof File) { writeFile(os, (File) body); } else if (body instanceof Map) { writeMultipart(os, (Map) body, boundary, true); } os.flush(); os.close(); } int statusCode = conn.getResponseCode(); response.setStatusCode(statusCode); if (DEBUG) { Log.d(TAG, "response code=" + statusCode); Log.d(TAG, "response length=" + conn.getContentLength()); } if (statusCode < ERROR_RESPONSE_CODE) { File tempFile = File.createTempFile("temp", ".dat"); FileOutputStream out = new FileOutputStream(tempFile); InputStream in = conn.getInputStream(); int len; byte[] buf = new byte[BUF_SIZE]; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } in.close(); response.setBody(tempFile); } else { if (DEBUG) { Log.w(TAG, "Failed to connect the server. response=" + statusCode); } File tempFile = File.createTempFile("temp", ".dat"); FileOutputStream out = new FileOutputStream(tempFile); InputStream in = conn.getErrorStream(); int len; byte[] buf = new byte[BUF_SIZE]; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } in.close(); response.setBody(tempFile); } } catch (IOException e) { if (DEBUG) { Log.e(TAG, "Failed to connect the server.", e); } } catch (Exception e) { if (DEBUG) { Log.e(TAG, "Failed to connect the server.", e); } } catch (OutOfMemoryError e) { if (DEBUG) { Log.e(TAG, "Failed to connect the server.", e); } } finally { if (conn != null) { conn.disconnect(); } } } /** * HTTP接続を行う. * @param method HTTPメソッド(GET, POST, PUT, DELETE) * @param uri 接続先のURI * @param headers ヘッダー * @param body ボディ * @return レスポンス */ public static Response connect(final String method, final String uri, final Map<String, String> headers, final Object body) { return connect(method, uri, headers, body, false); } /** * HTTP接続を行う. * @param method HTTPメソッド(GET, POST, PUT, DELETE) * @param uri 接続先のURI * @param headers ヘッダー * @param body ボディ * @param invalidDataFlag 不正なデータ作成フラグ * @return レスポンス */ public static Response connect(final String method, final String uri, final Map<String, String> headers, final Object body, final boolean invalidDataFlag) { Response response = new Response(); connect(response, method, uri, headers, body, invalidDataFlag); return response; } /** * GETメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @return レスポンス */ public static Response get(final String uri) { return connect(METHOD_GET, uri, null, null, false); } /** * GETメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @param headers HTTPヘッダー * @return レスポンス */ public static Response get(final String uri, final Map<String, String> headers) { return connect(METHOD_GET, uri, headers, null, false); } /** * PUTメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @return レスポンス */ public static Response put(final String uri) { return put(uri, null); } /** * PUTメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @param headers HTTPヘッダー * @return レスポンス */ public static Response put(final String uri, final Map<String, String> headers) { return put(uri, headers, null); } /** * PUTメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @param headers HTTPヘッダー * @param body HTTPボディ * @return レスポンス */ public static Response put(final String uri, final Map<String, String> headers, final Object body) { return connect(METHOD_PUT, uri, headers, body, false); } /** * POSTメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @param headers HTTPヘッダー * @return レスポンス */ public static Response post(final String uri, final Map<String, String> headers) { return post(uri, headers, null); } /** * POSTメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @param headers HTTPヘッダー * @param body 送信するHTTPボディ * @return レスポンス */ public static Response post(final String uri, Map<String, String> headers, final Object body) { return connect(METHOD_POST, uri, headers, body, false); } /** * POSTメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @param body 送信するHTTPボディ * @param invalidDataFlag 不正なデータ作成フラグ * @return レスポンス */ public static Response post(final String uri, final Object body, final boolean invalidDataFlag) { return connect(METHOD_POST, uri, null, body, invalidDataFlag); } /** * DELETEメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @return レスポンス */ public static Response delete(final String uri) { return connect(METHOD_DELETE, uri, null, null, false); } /** * DELETEメソッドで通信を行いレスポンスを取得する. * @param uri 接続先のURI * @param headers HTTPヘッダー * @return レスポンス */ public static Response delete(final String uri, final Map<String, String> headers) { return connect(METHOD_DELETE, uri, headers, null, false); } /** * HTTPレスポンスを格納するクラス. */ public static class Response { /** * ステータスコード. */ private int mStatusCode; /** * レスポンスのボディを保存するファイル. */ private File mBody; /** * ステータスコードを取得する. * @return ステータスコード */ public int getStatusCode() { return mStatusCode; } /** * ステータスコードを設定する. * @param statusCode ステータスコード */ void setStatusCode(final int statusCode) { mStatusCode = statusCode; } /** * レスポンスのボディへのファイルを取得する. * @return レスポンスのボディへのファイル */ public File getBody() { return mBody; } /** * レスポンスのボディへのファイルを設定する. * @param body レスポンスのボディへのファイル */ void setBody(final File body) { mBody = body; } public JSONObject getJSONObject() { if (mBody == null) { return null; } return getResponse(mBody); } private JSONObject getResponse(final File file) { ByteArrayOutputStream out = new ByteArrayOutputStream(); int len; byte[] buf = new byte[4096]; InputStream in = null; try { in = new FileInputStream(file); while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } String json = new String(out.toByteArray()); return new JSONObject(json); } catch (IOException e) { e.printStackTrace(); } catch (JSONException e) { e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } } }