package com.stripe.net; import com.stripe.Stripe; import com.stripe.exception.APIConnectionException; import com.stripe.exception.APIException; import com.stripe.exception.AuthenticationException; import com.stripe.exception.PermissionException; import com.stripe.exception.RateLimitException; import com.stripe.exception.CardException; import com.stripe.exception.InvalidRequestException; import com.stripe.model.StripeCollectionInterface; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.PasswordAuthentication; import java.net.URL; import java.net.URLStreamHandler; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Scanner; public class LiveStripeResponseGetter implements StripeResponseGetter { private static final String DNS_CACHE_TTL_PROPERTY_NAME = "networkaddress.cache.ttl"; private final static class Parameter { public final String key; public final String value; public Parameter(String key, String value) { this.key = key; this.value = value; } } /* * Set this property to override your environment's default * URLStreamHandler; Settings the property should not be needed in most * environments. */ private static final String CUSTOM_URL_STREAM_HANDLER_PROPERTY_NAME = "com.stripe.net.customURLStreamHandler"; private static final SSLSocketFactory socketFactory = new StripeSSLSocketFactory(); public <T> T request( APIResource.RequestMethod method, String url, Map<String, Object> params, Class<T> clazz, APIResource.RequestType type, RequestOptions options) throws AuthenticationException, InvalidRequestException, APIConnectionException, CardException, APIException { return _request(method, url, params, clazz, type, options); } private static String urlEncodePair(String k, String v) throws UnsupportedEncodingException { return String.format("%s=%s", APIResource.urlEncode(k), APIResource.urlEncode(v)); } static Map<String, String> getHeaders(RequestOptions options) { Map<String, String> headers = new HashMap<String, String>(); String apiVersion = options.getStripeVersion(); headers.put("Accept-Charset", APIResource.CHARSET); headers.put("Accept", "application/json"); headers.put("User-Agent", String.format("Stripe/v1 JavaBindings/%s", Stripe.VERSION)); headers.put("Authorization", String.format("Bearer %s", options.getApiKey())); // 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", APIResource.GSON.toJson(propertyMap)); if (apiVersion != null) { headers.put("Stripe-Version", apiVersion); } if (options.getIdempotencyKey() != null) { headers.put("Idempotency-Key", options.getIdempotencyKey()); } if (options.getStripeAccount() != null) { headers.put("Stripe-Account", options.getStripeAccount()); } return headers; } private static java.net.HttpURLConnection createStripeConnection( String url, RequestOptions options) throws IOException { URL stripeURL; String customURLStreamHandlerClassName = System.getProperty( CUSTOM_URL_STREAM_HANDLER_PROPERTY_NAME, null); if (customURLStreamHandlerClassName != null) { // instantiate the custom handler provided try { Class<URLStreamHandler> clazz = (Class<URLStreamHandler>) Class .forName(customURLStreamHandlerClassName); Constructor<URLStreamHandler> constructor = clazz .getConstructor(); URLStreamHandler customHandler = constructor.newInstance(); stripeURL = new URL(null, url, customHandler); } catch (ClassNotFoundException e) { throw new IOException(e); } catch (SecurityException e) { throw new IOException(e); } catch (NoSuchMethodException e) { throw new IOException(e); } catch (IllegalArgumentException e) { throw new IOException(e); } catch (InstantiationException e) { throw new IOException(e); } catch (IllegalAccessException e) { throw new IOException(e); } catch (InvocationTargetException e) { throw new IOException(e); } } else { stripeURL = new URL(url); } HttpURLConnection conn; if (Stripe.getConnectionProxy() != null) { conn = (HttpURLConnection) stripeURL.openConnection(Stripe.getConnectionProxy()); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return Stripe.getProxyCredential(); } }); } else { conn = (HttpURLConnection) stripeURL.openConnection(); } conn.setConnectTimeout(options.getConnectTimeout()); conn.setReadTimeout(options.getReadTimeout()); conn.setUseCaches(false); for (Map.Entry<String, String> header : getHeaders(options).entrySet()) { conn.setRequestProperty(header.getKey(), header.getValue()); } if (conn instanceof HttpsURLConnection) { ((HttpsURLConnection) conn).setSSLSocketFactory(socketFactory); } return conn; } private static String formatURL(String url, String query) { if (query == null || query.isEmpty()) { return url; } else { // In some cases, URL can already contain a question mark (eg, upcoming invoice lines) String separator = url.contains("?") ? "&" : "?"; return String.format("%s%s%s", url, separator, query); } } private static java.net.HttpURLConnection createGetConnection( String url, String query, RequestOptions options) throws IOException { String getURL = formatURL(url, query); java.net.HttpURLConnection conn = createStripeConnection(getURL, options); conn.setRequestMethod("GET"); return conn; } private static java.net.HttpURLConnection createPostConnection( String url, String query, RequestOptions options) throws IOException { java.net.HttpURLConnection conn = createStripeConnection(url, options); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", String.format( "application/x-www-form-urlencoded;charset=%s", APIResource.CHARSET)); OutputStream output = null; try { output = conn.getOutputStream(); output.write(query.getBytes(APIResource.CHARSET)); } finally { if (output != null) { output.close(); } } return conn; } private static java.net.HttpURLConnection createDeleteConnection( String url, String query, RequestOptions options) throws IOException { String deleteURL = formatURL(url, query); java.net.HttpURLConnection conn = createStripeConnection( deleteURL, options); conn.setRequestMethod("DELETE"); return conn; } static String createQuery(Map<String, Object> params) throws UnsupportedEncodingException, InvalidRequestException { StringBuilder queryStringBuffer = new StringBuilder(); List<Parameter> flatParams = flattenParams(params); Iterator<Parameter> it = flatParams.iterator(); while (it.hasNext()) { if (queryStringBuffer.length() > 0) { queryStringBuffer.append("&"); } Parameter param = it.next(); queryStringBuffer.append(urlEncodePair(param.key, param.value)); } return queryStringBuffer.toString(); } private static List<Parameter> flattenParams(Map<String, Object> params) throws InvalidRequestException { return flattenParamsMap(params, null); } private static List<Parameter> flattenParamsList(List<Object> params, String keyPrefix) throws InvalidRequestException { List<Parameter> flatParams = new LinkedList<Parameter>(); Iterator<?> it = ((List<?>)params).iterator(); String newPrefix = String.format("%s[]", keyPrefix); // Because application/x-www-form-urlencoded cannot represent an empty // list, convention is to take the list parameter and just set it to an // empty string. (e.g. A regular list might look like `a[]=1&b[]=2`. // Emptying it would look like `a=`.) if (params.isEmpty()) { flatParams.add(new Parameter(keyPrefix, "")); } else { while (it.hasNext()) { flatParams.addAll(flattenParamsValue(it.next(), newPrefix)); } } return flatParams; } private static List<Parameter> flattenParamsMap(Map<String, Object> params, String keyPrefix) throws InvalidRequestException { List<Parameter> flatParams = new LinkedList<Parameter>(); if (params == null) { return flatParams; } for (Map.Entry<String, Object> entry : params.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); String newPrefix = key; if (keyPrefix != null) { newPrefix = String.format("%s[%s]", keyPrefix, key); } flatParams.addAll(flattenParamsValue(value, newPrefix)); } return flatParams; } private static List<Parameter> flattenParamsValue(Object value, String keyPrefix) throws InvalidRequestException { List<Parameter> flatParams = new LinkedList<Parameter>(); if (value instanceof Map<?, ?>) { flatParams = flattenParamsMap((Map<String, Object>)value, keyPrefix); } else if (value instanceof List<?>) { flatParams = flattenParamsList((List<Object>)value, keyPrefix); } else if ("".equals(value)) { throw new InvalidRequestException("You cannot set '"+keyPrefix+"' to an empty string. "+ "We interpret empty strings as null in requests. "+ "You may set '"+keyPrefix+"' to null to delete the property.", keyPrefix, null, 0, null); } else if (value == null) { flatParams = new LinkedList<Parameter>(); flatParams.add(new Parameter(keyPrefix, "")); } else { flatParams = new LinkedList<Parameter>(); flatParams.add(new Parameter(keyPrefix, value.toString())); } return flatParams; } // represents Errors returned as JSON private static class ErrorContainer { private LiveStripeResponseGetter.Error error; } private static class Error { @SuppressWarnings("unused") String type; String message; String code; String param; String decline_code; String charge; } private static String getResponseBody(InputStream responseStream) throws IOException { //\A is the beginning of // the stream boundary String rBody = new Scanner(responseStream, APIResource.CHARSET) .useDelimiter("\\A") .next(); // responseStream.close(); return rBody; } private static StripeResponse makeURLConnectionRequest( APIResource.RequestMethod method, String url, String query, RequestOptions options) throws APIConnectionException { java.net.HttpURLConnection conn = null; try { switch (method) { case GET: conn = createGetConnection(url, query, options); break; case POST: conn = createPostConnection(url, query, options); break; case DELETE: conn = createDeleteConnection(url, query, options); 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)); } // trigger the request int rCode = conn.getResponseCode(); String rBody; Map<String, List<String>> headers; if (rCode >= 200 && rCode < 300) { rBody = getResponseBody(conn.getInputStream()); } else { rBody = getResponseBody(conn.getErrorStream()); } headers = conn.getHeaderFields(); return new StripeResponse(rCode, rBody, headers); } catch (IOException e) { throw new APIConnectionException( String.format( "IOException during API request to Stripe (%s): %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.getApiBase(), e.getMessage()), e); } finally { if (conn != null) { conn.disconnect(); } } } private static <T> T _request(APIResource.RequestMethod method, String url, Map<String, Object> params, Class<T> clazz, APIResource.RequestType type, RequestOptions options) throws AuthenticationException, InvalidRequestException, APIConnectionException, CardException, APIException { if (options == null) { options = RequestOptions.getDefault(); } String originalDNSCacheTTL = null; Boolean allowedToSetTTL = true; try { originalDNSCacheTTL = java.security.Security .getProperty(DNS_CACHE_TTL_PROPERTY_NAME); // disable DNS cache java.security.Security .setProperty(DNS_CACHE_TTL_PROPERTY_NAME, "0"); } catch (SecurityException se) { allowedToSetTTL = false; } String apiKey = options.getApiKey(); if (apiKey == null || apiKey.trim().isEmpty()) { 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.", null, 0); } try { StripeResponse response; switch (type) { case NORMAL: response = getStripeResponse(method, url, params, options); break; case MULTIPART: response = getMultipartStripeResponse(method, url, params, options); break; default: throw new RuntimeException( "Invalid APIResource request type. " + "This indicates a bug in the Stripe bindings. Please contact " + "support@stripe.com for assistance."); } int rCode = response.responseCode; String rBody = response.responseBody; String requestId = null; Map<String, List<String>> headers = response.getResponseHeaders(); List<String> requestIdList = headers == null ? null : headers.get("Request-Id"); if (requestIdList != null && requestIdList.size() > 0) { requestId = requestIdList.get(0); } if (rCode < 200 || rCode >= 300) { handleAPIError(rBody, rCode, requestId); } T resource = APIResource.GSON.fromJson(rBody, clazz); return resource; } finally { if (allowedToSetTTL) { if (originalDNSCacheTTL == null) { // value unspecified by implementation // DNS_CACHE_TTL_PROPERTY_NAME of -1 = cache forever java.security.Security.setProperty( DNS_CACHE_TTL_PROPERTY_NAME, "-1"); } else { java.security.Security.setProperty( DNS_CACHE_TTL_PROPERTY_NAME, originalDNSCacheTTL); } } } } private static StripeResponse getStripeResponse( APIResource.RequestMethod method, String url, Map<String, Object> params, RequestOptions options) throws InvalidRequestException, APIConnectionException, APIException { String query; try { query = createQuery(params); } catch (UnsupportedEncodingException e) { throw new InvalidRequestException("Unable to encode parameters to " + APIResource.CHARSET + ". Please contact support@stripe.com for assistance.", null, null, 0, e); } try { // HTTPSURLConnection verifies SSL cert by default return makeURLConnectionRequest(method, url, query, options); } catch (ClassCastException ce) { // appengine doesn't have HTTPSConnection, use URLFetch API String appEngineEnv = System.getProperty( "com.google.appengine.runtime.environment", null); if (appEngineEnv != null) { return makeAppEngineRequest(method, url, query, options); } else { // non-appengine ClassCastException throw ce; } } } private static StripeResponse getMultipartStripeResponse( APIResource.RequestMethod method, String url, Map<String, Object> params, RequestOptions options) throws InvalidRequestException, APIConnectionException, APIException { if (method != APIResource.RequestMethod.POST) { throw new InvalidRequestException( "Multipart requests for HTTP methods other than POST " + "are currently not supported.", null, null, 0, null); } java.net.HttpURLConnection conn = null; try { conn = createStripeConnection(url, options); String boundary = MultipartProcessor.getBoundary(); conn.setDoOutput(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", String.format( "multipart/form-data; boundary=%s", boundary)); MultipartProcessor multipartProcessor = null; try { multipartProcessor = new MultipartProcessor( conn, boundary, APIResource.CHARSET); for (Map.Entry<String, Object> entry : params.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (value instanceof File) { File currentFile = (File) value; if (!currentFile.exists()) { throw new InvalidRequestException("File for key " + key + " must exist.", null, null, 0, null); } else if (!currentFile.isFile()) { throw new InvalidRequestException("File for key " + key + " must be a file and not a directory.", null, null, 0, null); } else if (!currentFile.canRead()) { throw new InvalidRequestException( "Must have read permissions on file for key " + key + ".", null, null, 0, null); } multipartProcessor.addFileField(key, currentFile); } else { // We only allow a single level of nesting for params // for multipart multipartProcessor.addFormField(key, (String) value); } } } finally { if (multipartProcessor != null) { multipartProcessor.finish(); } } // trigger the request int rCode = conn.getResponseCode(); String rBody; Map<String, List<String>> headers; if (rCode >= 200 && rCode < 300) { rBody = getResponseBody(conn.getInputStream()); } else { rBody = getResponseBody(conn.getErrorStream()); } headers = conn.getHeaderFields(); return new StripeResponse(rCode, rBody, headers); } catch (IOException e) { throw new APIConnectionException( String.format( "IOException during API request to Stripe (%s): %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.getApiBase(), e.getMessage()), e); } finally { if (conn != null) { conn.disconnect(); } } } private static void handleAPIError(String rBody, int rCode, String requestId) throws InvalidRequestException, AuthenticationException, CardException, APIException { LiveStripeResponseGetter.Error error = APIResource.GSON.fromJson(rBody, LiveStripeResponseGetter.ErrorContainer.class).error; switch (rCode) { case 400: throw new InvalidRequestException(error.message, error.param, requestId, rCode, null); case 404: throw new InvalidRequestException(error.message, error.param, requestId, rCode, null); case 401: throw new AuthenticationException(error.message, requestId, rCode); case 402: throw new CardException(error.message, requestId, error.code, error.param, error.decline_code, error.charge, rCode, null); case 403: throw new PermissionException(error.message, requestId, rCode); case 429: throw new RateLimitException(error.message, error.param, requestId, rCode, null); default: throw new APIException(error.message, requestId, rCode, null); } } /* * This is slower than usual because of reflection but avoids having to * maintain AppEngine-specific JAR */ private static StripeResponse makeAppEngineRequest(APIResource.RequestMethod method, String url, String query, RequestOptions options) throws APIException { 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 == APIResource.RequestMethod.GET || method == APIResource.RequestMethod.DELETE) { 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; 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 == APIResource.RequestMethod.POST) { requestClass.getDeclaredMethod("setPayload", byte[].class) .invoke(request, query.getBytes()); } for (Map.Entry<String, String> header : getHeaders(options) .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), APIResource.CHARSET); return new StripeResponse(responseCode, body); } catch (InvocationTargetException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (MalformedURLException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (NoSuchFieldException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (SecurityException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (NoSuchMethodException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (ClassNotFoundException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (IllegalArgumentException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (IllegalAccessException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (InstantiationException e) { throw new APIException(unknownErrorMessage, null, 0, e); } catch (UnsupportedEncodingException e) { throw new APIException(unknownErrorMessage, null, 0, e); } } }