package com.android.feedmeandroid.net; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; import java.util.Scanner; import javax.net.ssl.HttpsURLConnection; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.android.feedmeandroid.Stripe; import com.android.feedmeandroid.exception.APIConnectionException; import com.android.feedmeandroid.exception.APIException; import com.android.feedmeandroid.exception.AuthenticationException; import com.android.feedmeandroid.exception.CardException; import com.android.feedmeandroid.exception.InvalidRequestException; import com.android.feedmeandroid.exception.StripeException; import com.android.feedmeandroid.model.EventData; import com.android.feedmeandroid.model.EventDataDeserializer; import com.android.feedmeandroid.model.StripeObject; public abstract class APIResource extends StripeObject { public static final Gson gson = new GsonBuilder(). setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES). registerTypeAdapter(EventData.class, new EventDataDeserializer()). create(); private static String className(Class<?> clazz) { return clazz.getSimpleName().toLowerCase().replace("$",""); } protected static String classURL(Class<?> clazz) { return String.format("%s/%ss", Stripe.API_BASE, className(clazz)); } protected static String instanceURL(Class<?> clazz, String id) { return String.format("%s/%s", classURL(clazz), id); } public static final String CHARSET = "UTF-8"; protected enum RequestMethod { GET, POST, DELETE } private static String urlEncodePair(String k, String v) throws UnsupportedEncodingException { return String.format("%s=%s", URLEncoder.encode(k, CHARSET), URLEncoder.encode(v, CHARSET)); } static Map<String, String> getHeaders() { Map<String, String> headers = new HashMap<String, String>(); headers.put("Accept-Charset", CHARSET); headers.put("User-Agent", String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION)); headers.put("Authorization", String.format("Bearer %s", Stripe.apiKey)); //debug headers String[] propertyNames = {"os.name", "os.version", "os.arch", "java.version", "java.vendor", "java.vm.version", "java.vm.vendor"}; Map<String, String> propertyMap = new HashMap<String, String>(); for(String propertyName: propertyNames) { propertyMap.put(propertyName, System.getProperty(propertyName)); } propertyMap.put("bindings.version", Stripe.VERSION); propertyMap.put("lang", "Java"); propertyMap.put("publisher", "Stripe"); headers.put("X-Stripe-Client-User-Agent", gson.toJson(propertyMap)); return headers; } private static HttpsURLConnection createStripeConnection(String url) throws IOException { URL stripeURL = new URL(url); HttpsURLConnection conn = (HttpsURLConnection) stripeURL.openConnection(); //enforce SSL URLs conn.setConnectTimeout(30000); // 30 seconds conn.setReadTimeout(80000); // 80 seconds conn.setUseCaches(false); for(Map.Entry<String, String> header: getHeaders().entrySet()) { conn.setRequestProperty(header.getKey(), header.getValue()); } return conn; } private static HttpsURLConnection createGetConnection(String url, String query) throws IOException { String getURL = String.format("%s?%s", url, query); HttpsURLConnection conn = createStripeConnection(getURL); conn.setRequestMethod("GET"); return conn; } private static HttpsURLConnection createPostConnection(String url, String query) throws IOException { HttpsURLConnection conn = createStripeConnection(url); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", String.format("application/x-www-form-urlencoded;charset=%s", CHARSET)); OutputStream output = null; try { output = conn.getOutputStream(); output.write(query.getBytes(CHARSET)); } finally { if (output != null) output.close(); } return conn; } private static HttpsURLConnection createDeleteConnection(String url, String query) throws IOException { String deleteUrl = String.format("%s?%s", url, query); HttpsURLConnection conn = createStripeConnection(deleteUrl); conn.setRequestMethod("DELETE"); return conn; } private static String createQuery(Map<String, Object> params) throws UnsupportedEncodingException { Map<String, String> flatParams = flattenParams(params); StringBuffer queryStringBuffer = new StringBuffer(); for(Map.Entry<String, String> entry: flatParams.entrySet()) { queryStringBuffer.append("&"); queryStringBuffer.append(urlEncodePair(entry.getKey(), entry.getValue())); } if (queryStringBuffer.length() > 0) queryStringBuffer.deleteCharAt(0); return queryStringBuffer.toString(); } private static Map<String, String> flattenParams(Map<String, Object> params) { if (params == null) { return new HashMap<String, String>(); } Map<String, String> flatParams = new HashMap<String, String>(); for(Map.Entry<String, Object> entry: params.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if(value instanceof Map<?, ?>) { Map<String, Object> flatNestedMap = new HashMap<String, Object>(); Map<?, ?> nestedMap = (Map<?, ?>)value; for(Map.Entry<?, ?> nestedEntry: nestedMap.entrySet()) { flatNestedMap.put(String.format("%s[%s]", key, nestedEntry.getKey()), nestedEntry.getValue()); } flatParams.putAll(flattenParams(flatNestedMap)); } else if (value != null) { flatParams.put(key, value.toString()); } } return flatParams; } //represents Errors returned as JSON private static class ErrorContainer { private APIResource.Error error; } private static class Error { @SuppressWarnings("unused") String type; String message; String code; String param; } private static String getResponseBody(InputStream responseStream) throws IOException { String rBody = new Scanner(responseStream, CHARSET).useDelimiter("\\A").next(); // \A is the beginning of the stream boundary responseStream.close(); return rBody; } private static StripeResponse makeURLConnectionRequest(APIResource.RequestMethod method, String url, String query) throws APIConnectionException { HttpsURLConnection conn = null; try { switch(method) { case GET: conn = createGetConnection(url, query); break; case POST: conn = createPostConnection(url, query); break; case DELETE: conn = createDeleteConnection(url, query); break; default: throw new APIConnectionException(String.format("Unrecognized HTTP method %s. " + "This indicates a bug in the Stripe bindings. Please contact " + "support@stripe.com for assistance.", method)); } int rCode = conn.getResponseCode(); //triggers the request String rBody = null; if (rCode >= 200 && rCode < 300) { rBody = getResponseBody(conn.getInputStream()); } else { rBody = getResponseBody(conn.getErrorStream()); } return new StripeResponse(rCode, rBody); } catch (IOException e) { throw new APIConnectionException(String.format("Could not connect to Stripe (%s). " + "Please check your internet connection and try again. If this problem persists," + "you should check Stripe's service status at https://twitter.com/stripestatus," + " or let us know at support@stripe.com.", Stripe.API_BASE), e); } finally { if (conn != null) { conn.disconnect(); } } } protected static <T> T request(APIResource.RequestMethod method, String url, Map<String, Object> params, Class<T> clazz) throws StripeException { if (Stripe.apiKey == null || Stripe.apiKey.length() == 0) { throw new AuthenticationException("No API key provided. (HINT: set your API key using 'Stripe.apiKey = <API-KEY>'. " + "You can generate API keys from the Stripe web interface. " + "See https://stripe.com/api for details or email support@stripe.com if you have questions."); } String query; try { query = createQuery(params); } catch (UnsupportedEncodingException e) { throw new InvalidRequestException("Unable to encode parameters to " + CHARSET + ". Please contact support@stripe.com for assistance.", null, e); } StripeResponse response; try { // HTTPSURLConnection verifies SSL cert by default response = makeURLConnectionRequest(method, url, query); } catch (ClassCastException ce) { // appengine doesn't have HTTPSConnection, use URLFetch API String appEngineEnv = System.getProperty("com.google.appengine.runtime.environment", null); if (appEngineEnv != null) { response = makeAppEngineRequest(method, url, query); } else { // non-appengine ClassCastException throw ce; } } int rCode = response.responseCode; String rBody = response.responseBody; if (rCode < 200 || rCode >= 300) { handleAPIError(rBody, rCode); } return gson.fromJson(rBody, clazz); } private static void handleAPIError(String rBody, int rCode) throws StripeException { APIResource.Error error = gson.fromJson(rBody, APIResource.ErrorContainer.class).error; switch(rCode) { case 400: throw new InvalidRequestException(error.message, error.param, null); case 404: throw new InvalidRequestException(error.message, error.param, null); case 401: throw new AuthenticationException(error.message); case 402: throw new CardException(error.message, error.code, error.param, null); default: throw new APIException(error.message, null); } } /* * This is slower than usual because of reflection * but avoids having to maintain AppEngine-specific JAR */ private static StripeResponse makeAppEngineRequest(RequestMethod method, String url, String query) throws StripeException { String unknownErrorMessage = "Sorry, an unknown error occurred while trying to use the " + "Google App Engine runtime. Please contact support@stripe.com for assistance."; try { if (method == RequestMethod.GET) { url = String.format("%s?%s", url, query); } URL fetchURL = new URL(url); Class<?> requestMethodClass = Class.forName("com.google.appengine.api.urlfetch.HTTPMethod"); Object httpMethod = requestMethodClass.getDeclaredField(method.name()).get(null); Class<?> fetchOptionsBuilderClass = Class.forName("com.google.appengine.api.urlfetch.FetchOptions$Builder"); Object fetchOptions = null; try { fetchOptions = fetchOptionsBuilderClass.getDeclaredMethod("validateCertificate").invoke(null); } catch (NoSuchMethodException e) { System.err.println("Warning: this App Engine SDK version does not allow verification of SSL certificates;" + "this exposes you to a MITM attack. Please upgrade your App Engine SDK to >=1.5.0. " + "If you have questions, contact support@stripe.com."); fetchOptions = fetchOptionsBuilderClass.getDeclaredMethod("withDefaults").invoke(null); } Class<?> fetchOptionsClass = Class.forName("com.google.appengine.api.urlfetch.FetchOptions"); // GAE requests can time out after 60 seconds, so make sure we leave // some time for the application to handle a slow Stripe fetchOptionsClass.getDeclaredMethod("setDeadline", java.lang.Double.class).invoke(fetchOptions, new Double(55)); Class<?> requestClass = Class.forName("com.google.appengine.api.urlfetch.HTTPRequest"); Object request = requestClass.getDeclaredConstructor(URL.class, requestMethodClass, fetchOptionsClass) .newInstance(fetchURL, httpMethod, fetchOptions); if (method == RequestMethod.POST) { requestClass.getDeclaredMethod("setPayload", byte[].class).invoke(request, query.getBytes()); } for(Map.Entry<String, String> header: getHeaders().entrySet()) { Class<?> httpHeaderClass = Class.forName("com.google.appengine.api.urlfetch.HTTPHeader"); Object reqHeader = httpHeaderClass.getDeclaredConstructor(String.class, String.class) .newInstance(header.getKey(), header.getValue()); requestClass.getDeclaredMethod("setHeader", httpHeaderClass).invoke(request, reqHeader); } Class<?> urlFetchFactoryClass = Class.forName("com.google.appengine.api.urlfetch.URLFetchServiceFactory"); Object urlFetchService = urlFetchFactoryClass.getDeclaredMethod("getURLFetchService").invoke(null); Method fetchMethod = urlFetchService.getClass().getDeclaredMethod("fetch", requestClass); fetchMethod.setAccessible(true); Object response = fetchMethod.invoke(urlFetchService, request); int responseCode = (Integer) response.getClass().getDeclaredMethod("getResponseCode").invoke(response); String body = new String((byte[]) response.getClass().getDeclaredMethod("getContent").invoke(response), CHARSET); return new StripeResponse(responseCode, body); } catch (InvocationTargetException e) { throw new APIException(unknownErrorMessage, e); } catch (MalformedURLException e) { throw new APIException(unknownErrorMessage, e); } catch (NoSuchFieldException e) { throw new APIException(unknownErrorMessage, e); } catch (SecurityException e) { throw new APIException(unknownErrorMessage, e); } catch (NoSuchMethodException e) { throw new APIException(unknownErrorMessage, e); } catch (ClassNotFoundException e) { throw new APIException(unknownErrorMessage, e); } catch (IllegalArgumentException e) { throw new APIException(unknownErrorMessage, e); } catch (IllegalAccessException e) { throw new APIException(unknownErrorMessage, e); } catch (InstantiationException e) { throw new APIException(unknownErrorMessage, e); } catch (UnsupportedEncodingException e) { throw new APIException(unknownErrorMessage, e); } } }