package org.commcare.network; import android.content.SharedPreferences; import android.net.Uri; import android.net.http.AndroidHttpClient; import android.util.Log; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.params.HttpClientParams; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.ExecutionContext; import org.apache.http.protocol.HttpContext; import org.commcare.CommCareApplication; import org.commcare.android.database.user.models.ACase; import org.commcare.cases.util.CaseDBUtils; import org.commcare.core.network.ModernHttpRequester; import org.commcare.interfaces.HttpRequestEndpoints; import org.commcare.logging.AndroidLogger; import org.commcare.models.database.SqlStorage; import org.commcare.preferences.CommCarePreferences; import org.commcare.preferences.DeveloperPreferences; import org.commcare.provider.DebugControlsReceiver; import org.commcare.utils.CredentialUtil; import org.javarosa.core.model.User; import org.javarosa.core.model.utils.DateUtils; import org.javarosa.core.services.Logger; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Date; import java.util.Vector; /** * @author ctsims */ public class HttpRequestGenerator implements HttpRequestEndpoints { private static final String TAG = HttpRequestGenerator.class.getSimpleName(); /** * A possible domain that further qualifies the username of any account in use */ public static final String USER_DOMAIN_SUFFIX = "cc_user_domain"; public static final String LOG_COMMCARE_NETWORK = "commcare-network"; /** * The type of authentication that we're capable of providing to the server (digest if this isn't present) */ public static final String AUTH_REQUEST_TYPE = "authtype"; /** * No Authentication will be possible, there isn't a user account to authenticate this request */ public static final String AUTH_REQUEST_TYPE_NO_AUTH = "noauth"; private static final String SUBMIT_MODE = "submit_mode"; private static final String SUBMIT_MODE_DEMO = "demo"; private final Credentials credentials; private final String username; private final String password; private final String userType; private final String userId; /** * Keep track of current request to allow for early aborting */ private HttpRequestBase currentRequest; public HttpRequestGenerator(User user) { this(user.getUsername(), user.getCachedPwd(), user.getUserType(), user.getUniqueId()); } public HttpRequestGenerator(String username, String password) { this(username, password, null); } public HttpRequestGenerator(String username, String password, String userId) { this(username, password, null, userId); } private HttpRequestGenerator(String username, String password, String userType, String userId) { String domainedUsername = buildDomainUser(username); this.password = password = buildAppPassword(password); this.userType = userType; if (username != null && !User.TYPE_DEMO.equals(userType)) { this.credentials = new UsernamePasswordCredentials(domainedUsername, password); this.username = username; } else { this.credentials = null; this.username = null; } this.userId = userId; } private String buildAppPassword(String password) { if (DeveloperPreferences.useObfuscatedPassword()) { return CredentialUtil.wrap(password); } return password; } protected static String buildDomainUser(String username) { if (username != null) { SharedPreferences prefs = CommCareApplication.instance().getCurrentApp().getAppPreferences(); if (prefs.contains(USER_DOMAIN_SUFFIX)) { username += "@" + prefs.getString(USER_DOMAIN_SUFFIX, null); } } return username; } public static HttpRequestGenerator buildNoAuthGenerator() { return new HttpRequestGenerator(null, null, null, null); } public HttpResponse get(String uri) throws ClientProtocolException, IOException { HttpClient client = client(); Log.d(TAG, "Fetching from: " + uri); HttpGet request = new HttpGet(uri); addHeaders(request, ""); HttpResponse response = execute(client, request); //May need to manually process a valid redirect if (response.getStatusLine().getStatusCode() == 301) { String newGetUri = request.getURI().toString(); Log.d(LOG_COMMCARE_NETWORK, "Following valid redirect from " + uri + " to " + newGetUri); request.abort(); //Make a new response to the redirect request = new HttpGet(newGetUri); addHeaders(request, ""); response = execute(client, request); } return response; } @Override public HttpResponse makeCaseFetchRequest(String baseUri, boolean includeStateFlags) throws ClientProtocolException, IOException { HttpClient client = client(); Uri serverUri = Uri.parse(baseUri); String vparam = serverUri.getQueryParameter("version"); if (vparam == null) { serverUri = serverUri.buildUpon().appendQueryParameter("version", "2.0").build(); } // include IMEI in key fetch request for auditing large deployments serverUri = serverUri.buildUpon().appendQueryParameter("device_id", CommCareApplication.instance().getPhoneId()).build(); if (userId != null) { serverUri = serverUri.buildUpon().appendQueryParameter("user_id", userId).build(); } String syncToken = null; if (includeStateFlags) { syncToken = getSyncToken(username); String digest = getDigest(); if (syncToken != null) { serverUri = serverUri.buildUpon().appendQueryParameter("since", syncToken).build(); } if (digest != null) { serverUri = serverUri.buildUpon().appendQueryParameter("state", "ccsh:" + digest).build(); } } //Add items count to fetch request serverUri = serverUri.buildUpon().appendQueryParameter("items", "true").build(); if (CommCareApplication.instance().shouldInvalidateCacheOnRestore()) { // Currently used for testing purposes only, in order to ensure that a full sync will // occur when we want to test one serverUri = serverUri.buildUpon().appendQueryParameter("overwrite_cache", "true").build(); // Always wipe this flag after we have used it once CommCareApplication.instance().setInvalidateCacheFlag(false); } String uri = serverUri.toString(); Log.d(TAG, "Fetching from: " + uri); currentRequest = new HttpGet(uri); AndroidHttpClient.modifyRequestToAcceptGzipResponse(currentRequest); addHeaders(currentRequest, syncToken); HttpResponse response = execute(client, currentRequest); currentRequest = null; return response; } @Override public HttpResponse makeKeyFetchRequest(String baseUri, Date lastRequest) throws ClientProtocolException, IOException { HttpClient client = client(); Uri url = Uri.parse(baseUri); if (lastRequest != null) { url = url.buildUpon().appendQueryParameter("last_issued", DateUtils.formatTime(lastRequest, DateUtils.FORMAT_ISO8601)).build(); } // include IMEI in key fetch request for auditing large deployments url = url.buildUpon().appendQueryParameter("device_id", CommCareApplication.instance().getPhoneId()).build(); HttpGet get = new HttpGet(url.toString()); return execute(client, get); } private void addHeaders(HttpRequestBase base, String lastToken) { //base.addHeader("Accept-Language", lang) base.addHeader("X-OpenRosa-Version", "2.0"); if (lastToken != null) { base.addHeader("X-CommCareHQ-LastSyncToken", lastToken); } base.addHeader("x-openrosa-deviceid", CommCareApplication.instance().getPhoneId()); } private String getSyncToken(String username) { if (username == null) { return null; } SqlStorage<User> storage = CommCareApplication.instance().getUserStorage(User.STORAGE_KEY, User.class); Vector<Integer> users = storage.getIDsForValue(User.META_USERNAME, username); //should be exactly one user if (users.size() != 1) { return null; } return storage.getMetaDataFieldForRecord(users.firstElement(), User.META_SYNC_TOKEN); } private static String getDigest() { String fakeHash = DebugControlsReceiver.getFakeCaseDbHash(); if (fakeHash != null) { // For integration tests, use fake hash to trigger 412 recovery on this sync return fakeHash; } else { return CaseDBUtils.computeCaseDbHash(CommCareApplication.instance().getUserStorage(ACase.STORAGE_KEY, ACase.class)); } } @Override public HttpResponse postData(String url, MultipartEntity entity) throws ClientProtocolException, IOException { // setup client HttpClient httpclient = client(); //If we're going to try to post with no credentials, we need to be explicit about the fact that we're //not ready if (credentials == null) { url = Uri.parse(url).buildUpon().appendQueryParameter(AUTH_REQUEST_TYPE, AUTH_REQUEST_TYPE_NO_AUTH).build().toString(); } if (User.TYPE_DEMO.equals(userType)) { url = Uri.parse(url).buildUpon().appendQueryParameter(SUBMIT_MODE, SUBMIT_MODE_DEMO).build().toString(); } HttpPost httppost = new HttpPost(url); httppost.setEntity(entity); addHeaders(httppost, this.getSyncToken(username)); return execute(httpclient, httppost); } private HttpClient client() { HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, ModernHttpRequester.CONNECTION_TIMEOUT); HttpConnectionParams.setSoTimeout(params, ModernHttpRequester.CONNECTION_SO_TIMEOUT); HttpClientParams.setRedirecting(params, true); DefaultHttpClient client = new DefaultHttpClient(params); client.getCredentialsProvider().setCredentials(AuthScope.ANY, credentials); System.setProperty("http.keepAlive", "false"); return client; } /** * Http requests are not so simple as "opening a request". Occasionally we may have to deal * with redirects. We don't want to just accept any redirect, though, since we may be directed * away from a secure connection. For now we'll only accept redirects from HTTP -> * servers, * or HTTPS -> HTTPS severs on the same domain */ private HttpResponse execute(HttpClient client, HttpUriRequest request) throws IOException { HttpContext context = new BasicHttpContext(); HttpResponse response = client.execute(request, context); HttpUriRequest currentReq = (HttpUriRequest)context.getAttribute(ExecutionContext.HTTP_REQUEST); HttpHost currentHost = (HttpHost)context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); String currentUrl = currentHost.toURI() + currentReq.getURI(); //Don't allow redirects _from_ https _to_ https unless they are redirecting to the same server. URL originalRequest = request.getURI().toURL(); URL finalRedirect = new URL(currentUrl); if (!isValidRedirect(originalRequest, finalRedirect)) { Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Invalid redirect from " + originalRequest.toString() + " to " + finalRedirect.toString()); throw new IOException("Invalid redirect from secure server to insecure server"); } return response; } public static boolean isValidRedirect(URL url, URL newUrl) { //unless it's https, don't worry about it if (!url.getProtocol().equals("https")) { return true; } // If https, verify that we're on the same server. // Not being so means we got redirected from a secure link to a // different link, which isn't acceptable for now. return url.getHost().equals(newUrl.getHost()); } @Override public InputStream simpleGet(URL url) throws IOException { if (android.os.Build.VERSION.SDK_INT > 11) { InputStream requestResult = SimpleGetRequest.makeRequest(username, password, url); if (requestResult != null) { return requestResult; } } // On earlier versions of android use the apache libraries, they work much much better. Log.i(LOG_COMMCARE_NETWORK, "Falling back to Apache libs for network request"); HttpResponse get = get(url.toString()); if (get.getStatusLine().getStatusCode() == 404) { throw new FileNotFoundException("No Data available at URL " + url.toString()); } //TODO: Double check response code return get.getEntity().getContent(); } @Override public void abortCurrentRequest() { if (currentRequest != null) { try { currentRequest.abort(); } catch (Exception e) { Log.i(TAG, "Error thrown while aborting http: " + e.getMessage()); } } } }