package org.commcare.android.util; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.ContentBody; import org.apache.http.entity.mime.content.FileBody; import org.commcare.android.database.user.models.User; import org.commcare.android.io.DataSubmissionEntity; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.mime.EncryptedFileBody; import org.commcare.android.net.HttpRequestGenerator; import org.commcare.android.tasks.DataSubmissionListener; import org.javarosa.core.io.StreamsUtil.InputIOException; import org.javarosa.core.services.Logger; import android.os.AsyncTask; import android.os.Environment; import android.util.Log; public class FormUploadUtil { /** Everything worked great! **/ public static final long FULL_SUCCESS = 0; /** There was a problem with the server's response **/ public static final long FAILURE = 2; /** There was a problem with the transport layer during transit **/ public static final long TRANSPORT_FAILURE = 4; /** There is a problem with this record that prevented submission success **/ public static final long RECORD_FAILURE = 8; public static final long SUBMISSION_BEGIN = 16; public static final long SUBMISSION_START = 32; public static final long SUBMISSION_NOTIFY = 64; public static final long SUBMISSION_DONE = 128; public static final long PROGRESS_LOGGED_OUT = 256; public static final long PROGRESS_SDCARD_REMOVED = 512; private static long MAX_BYTES = (5 * 1048576)-1024; private static final String[] SUPPORTED_FILE_EXTS = {".xml", ".jpg", "jpeg", ".3gpp", ".3gp", ".3ga", ".3g2", ".mp3", ".wav", ".amr",".mp4", ".3gp2", ".mpg4", ".mpeg4", ".m4v", ".mpg", ".mpeg", ".qcp"}; public static Cipher getDecryptCipher(SecretKeySpec key) { Cipher cipher; try { cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); return cipher; //TODO: Something smart here; } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchPaddingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvalidKeyException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } public static Cipher getDecryptCipher(byte[] key) { Cipher cipher; try { cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES")); return cipher; //TODO: Something smart here; } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (NoSuchPaddingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InvalidKeyException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } public static long sendInstance(int submissionNumber, File folder, String url, User user) throws FileNotFoundException { return FormUploadUtil.sendInstance(submissionNumber, folder, null, url, null, user); } public static long sendInstance(int submissionNumber, File folder, SecretKeySpec key, String url, AsyncTask listener, User user) throws FileNotFoundException { boolean hasListener = false; DataSubmissionListener myListener = null; if(listener instanceof DataSubmissionListener){ hasListener = true; myListener = (DataSubmissionListener)listener; } File[] files = folder.listFiles(); if(files == null) { //make sure external storage is available to begin with. String state = Environment.getExternalStorageState(); if (!Environment.MEDIA_MOUNTED.equals(state)) { //If so, just bail as if the user had logged out. throw new SessionUnavailableException("External Storage Removed"); } else { throw new FileNotFoundException("No directory found at: " + folder.getAbsoluteFile()); } } //If we're listening, figure out how much (roughly) we have to send long bytes = 0; for (int j = 0; j < files.length; j++) { //Make sure we'll be sending it boolean supported = false; for(String ext : SUPPORTED_FILE_EXTS) { if(files[j].getName().endsWith(ext)) { supported = true; break; } } if(!supported) { continue;} bytes += files[j].length(); System.out.println("Added file: " + files[j].getName() +". Bytes to send: " + bytes); } if(hasListener){ myListener.startSubmission(submissionNumber, bytes); } HttpRequestGenerator generator; if(user.getUserType().equals(User.TYPE_DEMO)) { generator = new HttpRequestGenerator(); } else { generator = new HttpRequestGenerator(user); } String t = "p+a+s"; if (files == null) { Log.e(t, "no files to upload"); listener.cancel(true); } // mime post MultipartEntity entity = new DataSubmissionEntity(myListener, submissionNumber); for (int j = 0; j < files.length; j++) { File f = files[j]; ContentBody fb; boolean supported = false; for(String ext : SUPPORTED_FILE_EXTS) { if(f.getName().endsWith(ext)) { supported = true; break; } } //TODO: Match this with some reasonable library, rather than silly file lines if (f.getName().endsWith(".xml")) { //fb = new InputStreamBody(new CipherInputStream(new FileInputStream(f), getDecryptCipher(aesKey)), "text/xml", f.getName()); if(key != null){ if(!validateSubmissionFile(f, FormUploadUtil.getDecryptCipher(key))) { return RECORD_FAILURE; } fb = new EncryptedFileBody(f, FormUploadUtil.getDecryptCipher(key), "text/xml"); } else{ fb = new FileBody(f, "text/xml"); } entity.addPart("xml_submission_file", fb); //fb = new FileBody(f, "text/xml"); //Don't know if we can ask for the content length on the input stream, so skip it. // if (fb.getContentLength() <= MAX_BYTES) { // Log.i(t, "added xml file " + f.getName()); // } else { // Log.i(t, "file " + f.getName() + " is too big"); // } } else if (f.getName().endsWith(".jpg")) { fb = new FileBody(f, "image/jpeg"); if (fb.getContentLength() <= MAX_BYTES) { entity.addPart(f.getName(), fb); Log.i(t, "added image file " + f.getName()); } else { Log.i(t, "file " + f.getName() + " is too big"); } } else if (f.getName().endsWith(".3gpp")) { fb = new FileBody(f, "audio/3gpp"); if (fb.getContentLength() <= MAX_BYTES) { entity.addPart(f.getName(), fb); Log.i(t, "added audio file " + f.getName()); } else { Log.i(t, "file " + f.getName() + " is too big"); } } else if (f.getName().endsWith(".3gp")) { fb = new FileBody(f, "video/3gpp"); if (fb.getContentLength() <= MAX_BYTES) { entity.addPart(f.getName(), fb); Log.i(t, "added video file " + f.getName()); } else { Log.i(t, "file " + f.getName() + " is too big"); } } else if (supported) { fb = new FileBody(f, "application/octet-stream"); if (fb.getContentLength() <= MAX_BYTES) { entity.addPart(f.getName(), fb); Log.i(t, "added unknown file " + f.getName()); } else { Log.i(t, "file " + f.getName() + " is too big"); } } else { Log.w(t, "unsupported file type, not adding file: " + f.getName()); } } // prepare response and return uploaded HttpResponse response = null; try { response = generator.postData(url, entity); } catch (InputIOException ioe ){ //This implies that there was a problem with the _source_ of the //transmission, not the processing or receiving end. Logger.log(AndroidLogger.TYPE_ERROR_STORAGE, "Internal error reading form record during submission: " + ioe.getWrapped().getMessage()); return RECORD_FAILURE; } catch (ClientProtocolException e) { e.printStackTrace(); return TRANSPORT_FAILURE; } catch (IOException e) { e.printStackTrace(); return TRANSPORT_FAILURE; } catch (IllegalStateException e) { e.printStackTrace(); return TRANSPORT_FAILURE; } String serverLocation = null; Header[] h = response.getHeaders("Location"); if (h != null && h.length > 0) { serverLocation = h[0].getValue(); } else { // something should be done here... Log.e(t, "Location header was absent"); } int responseCode = response.getStatusLine().getStatusCode(); Log.e(t, "Response code:" + responseCode); //If this response code wasn't legit if(!(responseCode >= 200 && responseCode < 300)) { //Log that so we can figure out what's up! Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Response Code: " + responseCode); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { AndroidStreamUtil.writeFromInputToOutput(response.getEntity().getContent(), bos); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } String responseString = new String(bos.toByteArray()); System.out.println(responseString); if(responseCode >= 200 && responseCode < 300) { return FULL_SUCCESS; } else { return FAILURE; } } /** * Validate the content body of the XML submission file. * * TODO: this should really be the responsibility of the form record, not of the * submission process, persay. * * NOTE: this is a shallow validation (everything should be more or else constant time). * Throws an exception if the file is gone because that's a common issue that gets caught * to check if storage got removed * * @param f * @param decryptCipher * @return * @throws FileNotFoundException */ public static boolean validateSubmissionFile(File f, Cipher decryptCipher) throws FileNotFoundException { if(!f.exists()) { throw new FileNotFoundException("Submission file: " + f.getAbsolutePath()); } //Gotta check f exists here since f.length returns 0 if the file isn't there for some reason. if(f.length() == 0 && f.exists()) { Logger.log(AndroidLogger.TYPE_ERROR_STORAGE, "Submission body has no content at: " + f.getAbsolutePath()); return false; } return true; } }