package org.azavea.otm.rest; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.text.TextUtils; import android.util.Base64; import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.AsyncHttpResponseHandler; import com.loopj.android.http.BinaryHttpResponseHandler; import com.loopj.android.http.JsonHttpResponseHandler; import com.loopj.android.http.RequestParams; import org.azavea.helpers.Logger; import org.azavea.otm.App; import org.azavea.otm.data.Model; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.security.SignatureException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.TimeZone; import cz.msebera.android.httpclient.Header; import cz.msebera.android.httpclient.entity.ByteArrayEntity; import cz.msebera.android.httpclient.entity.StringEntity; import cz.msebera.android.httpclient.message.BasicHeader; // This class is designed to take care of the base-url // and otm api-key for REST requests public class RestClient { private static final int NUM_OF_RETRIES = 3; private static final int TIMEOUT_IN_MILLIS = 4000; // 4 seconds private static final int TIMEOUT_BETWEEN_RETRIES = 1500; // 1.5 seconds private final String apiUrl; private final String baseUrl; private String host; private AsyncHttpClient client; private final SharedPreferences prefs; private final String appVersion; private final RequestSignature reqSigner; public RestClient() { prefs = App.getSharedPreferences(); apiUrl = getApiUrl(); baseUrl = getBaseUrl(); appVersion = getAppVersion(); client = createHttpClient(); reqSigner = new RequestSignature(prefs.getString("secret_key", "")); // The underlying request mechanism doesn't appear to set the HOST // header correctly, so include the header manually - it is required // to generate a matching signature on the api server. try { // Authority is servername[:port] if port is not 80 host = new URI(apiUrl).getAuthority(); } catch (URISyntaxException e) { Logger.error("Could not determine valid HOST from base URL", e); } } // Dependency injection to support mocking // in unit-tests public void setAsyncClient(AsyncHttpClient client) { this.client = client; } public void cancelRequests(Context context) { client.cancelRequests(context, true); } private void get(String url, RequestParams params, ArrayList<Header> headers, AsyncHttpResponseHandler responseHandler) { if (headers == null) { headers = new ArrayList<>(); } String reqUrl = getAbsoluteUrl(url); RequestParams reqParams = prepareParams(params); try { headers.add(reqSigner.getSignatureHeader("GET", reqUrl, reqParams)); } catch (UnsupportedEncodingException | URISyntaxException | SignatureException e) { Logger.error("Failure making GET request", e); return; } Header[] fullHeaders = prepareHeaders(headers); client.get(App.getAppInstance(), reqUrl, fullHeaders, reqParams, responseHandler); } /** * Signed GET request with no authentication */ public void get(String url, RequestParams params, AsyncHttpResponseHandler responseHandler) { this.get(url, params, null, responseHandler); } /** * Signed GET request with basic authentication headers */ public void getWithAuthentication(String url, String username, String password, RequestParams params, AsyncHttpResponseHandler responseHandler) { Header[] authHeader = {createBasicAuthenticationHeader(username, password)}; this.get(url, params, new ArrayList<>(Arrays.asList(authHeader)), responseHandler); } public void post(Context context, String url, Model model, AsyncHttpResponseHandler response) { post(url, null, model.getData().toString(), response); } private void put(String url, int id, ArrayList<Header> headers, String body, AsyncHttpResponseHandler responseHandler) { String reqUrl = safePathJoin(getAbsoluteUrl(url), id == -1 ? "" : Integer.toString(id)); String reqUrlWithParams = prepareUrl(reqUrl); if (headers == null) { headers = new ArrayList<>(); } StringEntity bodyEntity; try { headers.add(reqSigner.getSignatureHeader("PUT", reqUrlWithParams, body)); bodyEntity = new StringEntity(body, "UTF-8"); } catch (UnsupportedEncodingException | URISyntaxException | SignatureException e) { Logger.error("Failure making PUT request", e); return; } Header[] fullHeaders = prepareHeaders(headers); client.put(App.getAppInstance(), reqUrlWithParams, fullHeaders, bodyEntity, "application/json", responseHandler); } public void put(String url, int id, Model model, AsyncHttpResponseHandler response) { put(url, id, null, model.getData().toString(), response); } /** * Executes a put request and adds basic authentication headers to the * request. */ public void putWithAuthentication(String url, String username, String password, int id, Model model, AsyncHttpResponseHandler response) { Header[] headers = {createBasicAuthenticationHeader(username, password)}; String body = model.getData().toString(); put(url, id, new ArrayList<>(Arrays.asList(headers)), body, response); } public void putWithAuthentication(String url, String username, String password, Model model, AsyncHttpResponseHandler response) { Header[] headers = {createBasicAuthenticationHeader(username, password)}; String body = model.getData().toString(); put(url, -1, new ArrayList<>(Arrays.asList(headers)), body, response); } private void post(String url, ArrayList<Header> headers, String body, AsyncHttpResponseHandler responseHandler) { String type = "POST"; final String reqUrlWithParams = getAbsoluteUrlwithParams(url); if (headers == null) { headers = new ArrayList<>(); } StringEntity bodyEntity; try { headers.add(reqSigner.getSignatureHeader(type, reqUrlWithParams, body)); bodyEntity = new StringEntity(body, "UTF-8"); } catch (UnsupportedEncodingException | URISyntaxException | SignatureException e) { Logger.error("Error creating signature on POST"); return; } Header[] fullHeaders = prepareHeaders(headers); client.post(App.getAppInstance(), reqUrlWithParams, fullHeaders, bodyEntity, "application/json", responseHandler); } private String getAbsoluteUrlwithParams(String url) { String reqUrl = getAbsoluteUrl(url); return prepareUrl(reqUrl); } /** * Executes a post request and adds basic authentication headers to the * request. */ public void postWithAuthentication(String url, String username, String password, Model model, AsyncHttpResponseHandler responseHandler) { Header[] headers = {createBasicAuthenticationHeader(username, password)}; String body = null; if (model != null) { body = model.getData().toString(); } post(url, new ArrayList<>(Arrays.asList(headers)), body, responseHandler); } public void postWithAuthentication(String url, String username, String password, AsyncHttpResponseHandler responseHandler) { postWithAuthentication(url, username, password, null, responseHandler); } // This overloading of the postWithAuthentication method takes a bitmap, and // posts it as an PNG HTTP Entity. public void postWithAuthentication(String url, Bitmap bm, String username, String password, JsonHttpResponseHandler responseHandler, int timeout) { String completeUrl = getAbsoluteUrl(url); completeUrl = prepareUrl(completeUrl); // Content type also needs to be pinned down in the Bitmap.compress // call, which is why I haven't exposed it as a parameter. String contentType = "image/jpeg"; // We need to coerce the bitmap into a ByteArrayEntity so that we can // post it. ByteArrayOutputStream bos = new ByteArrayOutputStream(); bm.compress(CompressFormat.JPEG, 55, bos); byte[] bitmapdata = bos.toByteArray(); ByteArrayEntity bae = new ByteArrayEntity(bitmapdata); // No signature for http client which takes a BitmapEntity and a headers // array, so creating a one-off client for this purpose AsyncHttpClient authenticatedClient = createAutheniticatedHttpClient( username, password); // Add the signature based on the base64 encoded representation of the bitmap Header sig; try { sig = reqSigner.getSignatureHeader("POST", completeUrl, bitmapdata); } catch (URISyntaxException | SignatureException e) { Logger.error("Error creating signature on POST"); return; } authenticatedClient.addHeader(sig.getName(), sig.getValue()); authenticatedClient.setTimeout(timeout); authenticatedClient.post(App.getAppInstance(), completeUrl, bae, contentType, responseHandler); } public void delete(String url, AsyncHttpResponseHandler responseHandler) { client.delete(getAbsoluteUrl(url), responseHandler); } /** * Executes a delete request and adds basic authentication headers to the * request. */ public void deleteWithAuthentication(Context context, String url, String username, String password, AsyncHttpResponseHandler responseHandler) { String completeUrl = getAbsoluteUrl(url); completeUrl = prepareUrl(completeUrl); Header[] headers = {createBasicAuthenticationHeader(username, password)}; client.delete(context, completeUrl, headers, responseHandler); } public void getImage(String imageUrl, BinaryHttpResponseHandler handler) { if (imageUrl.startsWith("/")) { imageUrl = safePathJoin(baseUrl, imageUrl); } client.get(imageUrl, handler); } private RequestParams prepareParams(RequestParams params) { // We'll always need a RequestParams object since we'll always // be sending credentials RequestParams reqParams; if (params == null) { reqParams = new RequestParams(); } else { reqParams = params; } reqParams.put("timestamp", getTimestamp()); reqParams.put("access_key", getAccessKey()); return reqParams; } private String prepareUrl(String url) { // Not all methods of AsynchHttpClient take a requestParams. // Sometimes we will need to put the api key and other data // directly in the URL. return url + "?" + getTimestampQuery() + "&" + getAccessKeyQuery(); } /** * Ensure all required headers are present * * @param additionalHeaders List of headers specific to a single request * @return Complete list of headers necessary for API request */ private Header[] prepareHeaders(ArrayList<Header> additionalHeaders) { BasicHeader defaultHeader = new BasicHeader("Host", host); if (additionalHeaders != null) { ArrayList<Header> headers = (ArrayList<Header>) additionalHeaders.clone(); headers.add(defaultHeader); return headers.toArray(new Header[headers.size()]); } else { return new Header[]{defaultHeader}; } } /** * Current timestamp string in UTC format for API request verification * * @return Query string format of "timestamp={UTC Timestamp}" */ private String getTimestampQuery() { return "timestamp=" + getTimestamp(); } /** * @return Current timestamp string in UTC format for API request * verification */ private String getTimestamp() { SimpleDateFormat dateFormatUtc = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); dateFormatUtc.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormatUtc.format(new Date()); } /** * Configured Access Key for API request verification * * @return Query string format of "access_key={ACCESSKEY}" */ private String getAccessKeyQuery() { return "access_key=" + getAccessKey(); } private String getAccessKey() { return prefs.getString("access_key", ""); } private String getApiUrl() { String apiUrl = prefs.getString("api_url", ""); return apiUrl; } private String getBaseUrl() { String baseUrl = prefs.getString("base_url", ""); return baseUrl; } private String getAbsoluteUrl(String relativeUrl) { return safePathJoin(apiUrl, relativeUrl); } private String safePathJoin(String base, String path) { String cleanBase = base; String cleanPath = path; if (base.charAt(base.length() - 1) == '/') { cleanBase = base.substring(0, base.length() - 1); } if (!TextUtils.isEmpty(path) && path.charAt(0) == '/') { cleanPath = path.substring(1); } else if (TextUtils.isEmpty(path)) { return cleanBase; } return cleanBase + "/" + cleanPath; } private Header createBasicAuthenticationHeader(String username, String password) { String credentials = String.format("%s:%s", username, password); String encoded = Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP); return new BasicHeader("Authorization", String.format("%s %s", "Basic", encoded)); } private AsyncHttpClient createHttpClient() { AsyncHttpClient client = new AsyncHttpClient(); client.addHeader("platform-ver-build", appVersion); client.setTimeout(TIMEOUT_IN_MILLIS); client.setMaxRetriesAndTimeout(NUM_OF_RETRIES, TIMEOUT_BETWEEN_RETRIES); return client; } private AsyncHttpClient createAutheniticatedHttpClient(String username, String password) { AsyncHttpClient client = createHttpClient(); Header header = createBasicAuthenticationHeader(username, password); client.addHeader(header.getName(), header.getValue()); return client; } private String getAppVersion() { return prefs.getString("platform_ver_build", ""); } }