From 5dacb8d2d5bb1e8a9806f07185f510a608586f15 Mon Sep 17 00:00:00 2001 From: Don Coleman Date: Thu, 17 May 2012 14:51:41 -0400 Subject: [PATCH] [CB-792] Add HTTP status code to FileTransferError --- .../src/org/apache/cordova/FileTransfer.java | 553 ++++++++++-------- 1 file changed, 305 insertions(+), 248 deletions(-) diff --git a/framework/src/org/apache/cordova/FileTransfer.java b/framework/src/org/apache/cordova/FileTransfer.java index a631a055..368d97ad 100644 --- a/framework/src/org/apache/cordova/FileTransfer.java +++ b/framework/src/org/apache/cordova/FileTransfer.java @@ -27,6 +27,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.URL; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -35,7 +36,6 @@ import java.util.Iterator; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; @@ -57,7 +57,7 @@ public class FileTransfer extends Plugin { private static final String LOG_TAG = "FileTransfer"; private static final String LINE_START = "--"; private static final String LINE_END = "\r\n"; - private static final String BOUNDRY = "*****"; + private static final String BOUNDARY = "*****"; public static int FILE_NOT_FOUND_ERR = 1; public static int INVALID_URL_ERR = 2; @@ -82,49 +82,251 @@ public class FileTransfer extends Plugin { return new PluginResult(PluginResult.Status.JSON_EXCEPTION, "Missing source or target"); } - try { - if (action.equals("upload")) { - // Setup the options - String fileKey = null; - String fileName = null; - String mimeType = null; + if (action.equals("upload")) { + return upload(source, target, args); + } else if (action.equals("download")) { + return download(source, target); + } else { + return new PluginResult(PluginResult.Status.INVALID_ACTION); + } + } - fileKey = getArgument(args, 2, "file"); - fileName = getArgument(args, 3, "image.jpg"); - mimeType = getArgument(args, 4, "image/jpeg"); - JSONObject params = args.optJSONObject(5); - boolean trustEveryone = args.optBoolean(6); - boolean chunkedMode = args.optBoolean(7) || args.isNull(7); //Always use chunked mode unless set to false as per API - FileUploadResult r = upload(source, target, fileKey, fileName, mimeType, params, trustEveryone, chunkedMode); - Log.d(LOG_TAG, "****** About to return a result from upload"); - return new PluginResult(PluginResult.Status.OK, r.toJSONObject()); - } else if (action.equals("download")) { - JSONObject r = download(source, target); - Log.d(LOG_TAG, "****** About to return a result from download"); - return new PluginResult(PluginResult.Status.OK, r); - } else { - return new PluginResult(PluginResult.Status.INVALID_ACTION); + /** + * Uploads the specified file to the server URL provided using an HTTP multipart request. + * @param source Full path of the file on the file system + * @param target URL of the server to receive the file + * @param args JSON Array of args + * + * args[2] fileKey Name of file request parameter + * args[3] fileName File name to be used on server + * args[4] mimeType Describes file content type + * args[5] params key:value pairs of user-defined parameters + * @return FileUploadResult containing result of upload request + */ + private PluginResult upload(String source, String target, JSONArray args) { + Log.d(LOG_TAG, "upload " + source + " to " + target); + + HttpURLConnection conn = null; + try { + // Setup the options + String fileKey = getArgument(args, 2, "file"); + String fileName = getArgument(args, 3, "image.jpg"); + String mimeType = getArgument(args, 4, "image/jpeg"); + JSONObject params = args.optJSONObject(5); + if (params == null) params = new JSONObject(); + boolean trustEveryone = args.optBoolean(6); + boolean chunkedMode = args.optBoolean(7) || args.isNull(7); //Always use chunked mode unless set to false as per API + + Log.d(LOG_TAG, "fileKey: " + fileKey); + Log.d(LOG_TAG, "fileName: " + fileName); + Log.d(LOG_TAG, "mimeType: " + mimeType); + Log.d(LOG_TAG, "params: " + params); + Log.d(LOG_TAG, "trustEveryone: " + trustEveryone); + Log.d(LOG_TAG, "chunkedMode: " + chunkedMode); + + // Create return object + FileUploadResult result = new FileUploadResult(); + + // Get a input stream of the file on the phone + FileInputStream fileInputStream = (FileInputStream) getPathFromUri(source); + + DataOutputStream dos = null; + + int bytesRead, bytesAvailable, bufferSize; + long totalBytes; + byte[] buffer; + int maxBufferSize = 8096; + + //------------------ CLIENT REQUEST + // open a URL connection to the server + URL url = new URL(target); + + // Open a HTTP connection to the URL based on protocol + if (url.getProtocol().toLowerCase().equals("https")) { + // Using standard HTTPS connection. Will not allow self signed certificate + if (!trustEveryone) { + conn = (HttpsURLConnection) url.openConnection(); + } + // Use our HTTPS connection that blindly trusts everyone. + // This should only be used in debug environments + else { + // Setup the HTTPS connection class to trust everyone + trustAllHosts(); + HttpsURLConnection https = (HttpsURLConnection) url.openConnection(); + // Save the current hostnameVerifier + defaultHostnameVerifier = https.getHostnameVerifier(); + // Setup the connection not to verify hostnames + https.setHostnameVerifier(DO_NOT_VERIFY); + conn = https; + } } + // Return a standard HTTP connection + else { + conn = (HttpURLConnection) url.openConnection(); + } + + // Allow Inputs + conn.setDoInput(true); + + // Allow Outputs + conn.setDoOutput(true); + + // Don't use a cached copy. + conn.setUseCaches(false); + + // Use a post method. + conn.setRequestMethod("POST"); + conn.setRequestProperty("Connection", "Keep-Alive"); + conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + BOUNDARY); + + // Handle the other headers + try { + JSONObject headers = params.getJSONObject("headers"); + for (Iterator iter = headers.keys(); iter.hasNext();) + { + String headerKey = iter.next().toString(); + conn.setRequestProperty(headerKey, headers.getString(headerKey)); + } + } catch (JSONException e1) { + // No headers to be manipulated! + } + + // Set the cookies on the response + String cookie = CookieManager.getInstance().getCookie(target); + if (cookie != null) { + conn.setRequestProperty("Cookie", cookie); + } + + + /* + * Store the non-file portions of the multipart data as a string, so that we can add it + * to the contentSize, since it is part of the body of the HTTP request. + */ + String extraParams = ""; + try { + for (Iterator iter = params.keys(); iter.hasNext();) { + Object key = iter.next(); + if(!String.valueOf(key).equals("headers")) + { + extraParams += LINE_START + BOUNDARY + LINE_END; + extraParams += "Content-Disposition: form-data; name=\"" + key.toString() + "\";"; + extraParams += LINE_END + LINE_END; + extraParams += params.getString(key.toString()); + extraParams += LINE_END; + } + } + } catch (JSONException e) { + Log.e(LOG_TAG, e.getMessage(), e); + } + + extraParams += LINE_START + BOUNDARY + LINE_END; + extraParams += "Content-Disposition: form-data; name=\"" + fileKey + "\";" + " filename=\""; + + String midParams = "\"" + LINE_END + "Content-Type: " + mimeType + LINE_END + LINE_END; + String tailParams = LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END; + + // Should set this up as an option + if (chunkedMode) { + conn.setChunkedStreamingMode(maxBufferSize); + } + else + { + int stringLength = extraParams.length() + midParams.length() + tailParams.length() + fileName.getBytes("UTF-8").length; + Log.d(LOG_TAG, "String Length: " + stringLength); + int fixedLength = (int) fileInputStream.getChannel().size() + stringLength; + Log.d(LOG_TAG, "Content Length: " + fixedLength); + conn.setFixedLengthStreamingMode(fixedLength); + } + + + dos = new DataOutputStream( conn.getOutputStream() ); + dos.writeBytes(extraParams); + //We don't want to chagne encoding, we just want this to write for all Unicode. + dos.write(fileName.getBytes("UTF-8")); + dos.writeBytes(midParams); + + // create a buffer of maximum size + bytesAvailable = fileInputStream.available(); + bufferSize = Math.min(bytesAvailable, maxBufferSize); + buffer = new byte[bufferSize]; + + // read file and write it into form... + bytesRead = fileInputStream.read(buffer, 0, bufferSize); + totalBytes = 0; + + while (bytesRead > 0) { + totalBytes += bytesRead; + result.setBytesSent(totalBytes); + dos.write(buffer, 0, bufferSize); + bytesAvailable = fileInputStream.available(); + bufferSize = Math.min(bytesAvailable, maxBufferSize); + bytesRead = fileInputStream.read(buffer, 0, bufferSize); + } + + // send multipart form data necesssary after file data... + dos.writeBytes(tailParams); + + // close streams + fileInputStream.close(); + dos.flush(); + dos.close(); + + //------------------ read the SERVER RESPONSE + StringBuffer responseString = new StringBuffer(""); + DataInputStream inStream; + try { + inStream = new DataInputStream ( conn.getInputStream() ); + } catch(FileNotFoundException e) { + Log.e(LOG_TAG, e.toString(), e); + throw new IOException("Received error from server"); + } + + String line; + while (( line = inStream.readLine()) != null) { + responseString.append(line); + } + Log.d(LOG_TAG, "got response from server"); + Log.d(LOG_TAG, responseString.toString()); + + // send request and retrieve response + result.setResponseCode(conn.getResponseCode()); + result.setResponse(responseString.toString()); + + inStream.close(); + + // Revert back to the proper verifier and socket factories + if (trustEveryone && url.getProtocol().toLowerCase().equals("https")) { + ((HttpsURLConnection) conn).setHostnameVerifier(defaultHostnameVerifier); + HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory); + } + + Log.d(LOG_TAG, "****** About to return a result from upload"); + return new PluginResult(PluginResult.Status.OK, result.toJSONObject()); + } catch (FileNotFoundException e) { - Log.e(LOG_TAG, e.getMessage(), e); - JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target); + JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, conn); + Log.e(LOG_TAG, error.toString(), e); return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); - } catch (IllegalArgumentException e) { - Log.e(LOG_TAG, e.getMessage(), e); - JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target); - return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); - } catch (SSLException e) { - Log.e(LOG_TAG, e.getMessage(), e); - Log.d(LOG_TAG, "Got my ssl exception!!!"); - JSONObject error = createFileTransferError(CONNECTION_ERR, source, target); + } catch (MalformedURLException e) { + JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, conn); + Log.e(LOG_TAG, error.toString(), e); return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); } catch (IOException e) { - Log.e(LOG_TAG, e.getMessage(), e); - JSONObject error = createFileTransferError(CONNECTION_ERR, source, target); + JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn); + Log.e(LOG_TAG, error.toString(), e); return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); } catch (JSONException e) { Log.e(LOG_TAG, e.getMessage(), e); return new PluginResult(PluginResult.Status.JSON_EXCEPTION); + } catch (Throwable t) { + // Shouldn't happen, but will + JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn); + Log.wtf(LOG_TAG, error.toString(), t); + return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); + } finally { + if (conn != null) { + conn.disconnect(); + } } } @@ -172,18 +374,36 @@ public class FileTransfer extends Plugin { } } - /** - * Create an error object based on the passed in errorCode - * @param errorCode the error - * @return JSONObject containing the error - */ - private JSONObject createFileTransferError(int errorCode, String source, String target) { + private JSONObject createFileTransferError(int errorCode, String source, String target, HttpURLConnection connection) { + + Integer httpStatus = null; + + if (connection != null) { + try { + httpStatus = connection.getResponseCode(); + } catch (IOException e) { + Log.w(LOG_TAG, "Error getting HTTP status code from connection.", e); + } + } + + return createFileTransferError(errorCode, source, target, httpStatus); + } + + /** + * Create an error object based on the passed in errorCode + * @param errorCode the error + * @return JSONObject containing the error + */ + private JSONObject createFileTransferError(int errorCode, String source, String target, Integer httpStatus) { JSONObject error = null; try { error = new JSONObject(); error.put("code", errorCode); error.put("source", source); error.put("target", target); + if (httpStatus != null) { + error.put("http_status", httpStatus); + } } catch (JSONException e) { Log.e(LOG_TAG, e.getMessage(), e); } @@ -208,198 +428,6 @@ public class FileTransfer extends Plugin { return arg; } - /** - * Uploads the specified file to the server URL provided using an HTTP - * multipart request. - * @param file Full path of the file on the file system - * @param server URL of the server to receive the file - * @param fileKey Name of file request parameter - * @param fileName File name to be used on server - * @param mimeType Describes file content type - * @param params key:value pairs of user-defined parameters - * @return FileUploadResult containing result of upload request - */ - public FileUploadResult upload(String file, String server, final String fileKey, final String fileName, - final String mimeType, JSONObject params, boolean trustEveryone, boolean chunkedMode) throws IOException, SSLException { - // Create return object - FileUploadResult result = new FileUploadResult(); - - // Get a input stream of the file on the phone - FileInputStream fileInputStream = (FileInputStream) getPathFromUri(file); - - HttpURLConnection conn = null; - DataOutputStream dos = null; - - int bytesRead, bytesAvailable, bufferSize; - long totalBytes; - byte[] buffer; - int maxBufferSize = 8096; - - //------------------ CLIENT REQUEST - // open a URL connection to the server - URL url = new URL(server); - - // Open a HTTP connection to the URL based on protocol - if (url.getProtocol().toLowerCase().equals("https")) { - // Using standard HTTPS connection. Will not allow self signed certificate - if (!trustEveryone) { - conn = (HttpsURLConnection) url.openConnection(); - } - // Use our HTTPS connection that blindly trusts everyone. - // This should only be used in debug environments - else { - // Setup the HTTPS connection class to trust everyone - trustAllHosts(); - HttpsURLConnection https = (HttpsURLConnection) url.openConnection(); - // Save the current hostnameVerifier - defaultHostnameVerifier = https.getHostnameVerifier(); - // Setup the connection not to verify hostnames - https.setHostnameVerifier(DO_NOT_VERIFY); - conn = https; - } - } - // Return a standard HTTP connection - else { - conn = (HttpURLConnection) url.openConnection(); - } - - // Allow Inputs - conn.setDoInput(true); - - // Allow Outputs - conn.setDoOutput(true); - - // Don't use a cached copy. - conn.setUseCaches(false); - - // Use a post method. - conn.setRequestMethod("POST"); - conn.setRequestProperty("Connection", "Keep-Alive"); - conn.setRequestProperty("Content-Type", "multipart/form-data;boundary="+BOUNDRY); - - // Handle the other headers - try { - JSONObject headers = params.getJSONObject("headers"); - for (Iterator iter = headers.keys(); iter.hasNext();) - { - String headerKey = iter.next().toString(); - conn.setRequestProperty(headerKey, headers.getString(headerKey)); - } - } catch (JSONException e1) { - // No headers to be manipulated! - } - - // Set the cookies on the response - String cookie = CookieManager.getInstance().getCookie(server); - if (cookie != null) { - conn.setRequestProperty("Cookie", cookie); - } - - - /* - * Store the non-file portions of the multipart data as a string, so that we can add it - * to the contentSize, since it is part of the body of the HTTP request. - */ - String extraParams = ""; - try { - for (Iterator iter = params.keys(); iter.hasNext();) { - Object key = iter.next(); - if(key.toString() != "headers") - { - extraParams += LINE_START + BOUNDRY + LINE_END; - extraParams += "Content-Disposition: form-data; name=\"" + key.toString() + "\";"; - extraParams += LINE_END + LINE_END; - extraParams += params.getString(key.toString()); - extraParams += LINE_END; - } - } - } catch (JSONException e) { - Log.e(LOG_TAG, e.getMessage(), e); - } - - extraParams += LINE_START + BOUNDRY + LINE_END; - extraParams += "Content-Disposition: form-data; name=\"" + fileKey + "\";" + " filename=\""; - - String midParams = "\"" + LINE_END + "Content-Type: " + mimeType + LINE_END + LINE_END; - String tailParams = LINE_END + LINE_START + BOUNDRY + LINE_START + LINE_END; - - // Should set this up as an option - if (chunkedMode) { - conn.setChunkedStreamingMode(maxBufferSize); - } - else - { - int stringLength = extraParams.length() + midParams.length() + tailParams.length() + fileName.getBytes("UTF-8").length; - Log.d(LOG_TAG, "String Length: " + stringLength); - int fixedLength = (int) fileInputStream.getChannel().size() + stringLength; - Log.d(LOG_TAG, "Content Length: " + fixedLength); - conn.setFixedLengthStreamingMode(fixedLength); - } - - - dos = new DataOutputStream( conn.getOutputStream() ); - dos.writeBytes(extraParams); - //We don't want to chagne encoding, we just want this to write for all Unicode. - dos.write(fileName.getBytes("UTF-8")); - dos.writeBytes(midParams); - - // create a buffer of maximum size - bytesAvailable = fileInputStream.available(); - bufferSize = Math.min(bytesAvailable, maxBufferSize); - buffer = new byte[bufferSize]; - - // read file and write it into form... - bytesRead = fileInputStream.read(buffer, 0, bufferSize); - totalBytes = 0; - - while (bytesRead > 0) { - totalBytes += bytesRead; - result.setBytesSent(totalBytes); - dos.write(buffer, 0, bufferSize); - bytesAvailable = fileInputStream.available(); - bufferSize = Math.min(bytesAvailable, maxBufferSize); - bytesRead = fileInputStream.read(buffer, 0, bufferSize); - } - - // send multipart form data necesssary after file data... - dos.writeBytes(tailParams); - - // close streams - fileInputStream.close(); - dos.flush(); - dos.close(); - - //------------------ read the SERVER RESPONSE - StringBuffer responseString = new StringBuffer(""); - DataInputStream inStream; - try { - inStream = new DataInputStream ( conn.getInputStream() ); - } catch(FileNotFoundException e) { - throw new IOException("Received error from server"); - } - - String line; - while (( line = inStream.readLine()) != null) { - responseString.append(line); - } - Log.d(LOG_TAG, "got response from server"); - Log.d(LOG_TAG, responseString.toString()); - - // send request and retrieve response - result.setResponseCode(conn.getResponseCode()); - result.setResponse(responseString.toString()); - - inStream.close(); - conn.disconnect(); - - // Revert back to the proper verifier and socket factories - if (trustEveryone && url.getProtocol().toLowerCase().equals("https")) { - ((HttpsURLConnection)conn).setHostnameVerifier(defaultHostnameVerifier); - HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory); - } - - return result; - } /** * Downloads a file form a given URL and saves it to the specified directory. @@ -408,7 +436,10 @@ public class FileTransfer extends Plugin { * @param target Full path of the file on the file system * @return JSONObject the downloaded file */ - public JSONObject download(String source, String target) throws IOException { + private PluginResult download(String source, String target) { + Log.d(LOG_TAG, "download " + source + " to " + target); + + HttpURLConnection connection = null; try { File file = getFileFromPath(target); @@ -419,7 +450,7 @@ public class FileTransfer extends Plugin { if(this.ctx.isUrlWhiteListed(source)) { URL url = new URL(source); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); //Add cookie support @@ -431,7 +462,7 @@ public class FileTransfer extends Plugin { connection.connect(); - Log.d(LOG_TAG, "Download file:" + url); + Log.d(LOG_TAG, "Download file: " + url); InputStream inputStream = connection.getInputStream(); byte[] buffer = new byte[1024]; @@ -450,23 +481,40 @@ public class FileTransfer extends Plugin { // create FileEntry object FileUtils fileUtil = new FileUtils(); + JSONObject fileEntry = fileUtil.getEntry(file); - return fileUtil.getEntry(file); + return new PluginResult(PluginResult.Status.OK, fileEntry); } else { - throw new IOException("Error: Unable to connect to domain"); + Log.w(LOG_TAG, "Source URL is not in white list: '" + source + "'"); + JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, 401); + return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); + } + + } catch (FileNotFoundException e) { + JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, connection); + Log.e(LOG_TAG, error.toString(), e); + return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); + } catch (MalformedURLException e) { + JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, connection); + Log.e(LOG_TAG, error.toString(), e); + return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); + } catch (Exception e) { // IOException, JSONException, NullPointer + JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection); + Log.e(LOG_TAG, error.toString(), e); + return new PluginResult(PluginResult.Status.IO_EXCEPTION, error); + } finally { + if (connection != null) { + connection.disconnect(); } - } catch (Exception e) { - Log.d(LOG_TAG, e.getMessage(), e); - throw new IOException("Error while downloading"); } } /** * Get an input stream based on file path or content:// uri * - * @param path + * @param path foo * @return an input stream * @throws FileNotFoundException */ @@ -491,14 +539,23 @@ public class FileTransfer extends Plugin { /** * Get a File object from the passed in path * - * @param path - * @return + * @param path file path + * @return file object */ - private File getFileFromPath(String path) { - if (path.startsWith("file://")) { - return new File(path.substring(7)); + private File getFileFromPath(String path) throws FileNotFoundException { + File file; + String prefix = "file://"; + + if (path.startsWith(prefix)) { + file = new File(path.substring(prefix.length())); } else { - return new File(path); + file = new File(path); } + + if (file.getParent() == null) { + throw new FileNotFoundException(); + } + + return file; } }