package org.commcare.android.net; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.PasswordAuthentication; import java.net.URL; import java.util.Date; import java.util.Vector; 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.android.database.SqlStorage; import org.commcare.android.database.user.models.ACase; import org.commcare.android.database.user.models.User; import org.commcare.android.javarosa.AndroidLogger; import org.commcare.android.logic.GlobalConstants; import org.commcare.cases.util.CaseDBUtils; import org.commcare.dalvik.application.CommCareApplication; import org.javarosa.core.model.utils.DateUtils; import org.javarosa.core.services.Logger; import android.content.SharedPreferences; import android.net.Uri; import android.util.Log; /** * @author ctsims * */ public class HttpRequestGenerator { /** 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 Credentials credentials; PasswordAuthentication passwordAuthentication; private String username; public HttpRequestGenerator(User user) { this(user.getUsername(), user.getCachedPwd()); } public HttpRequestGenerator(String username, String password) { String domainedUsername = username; SharedPreferences prefs = CommCareApplication._().getCurrentApp().getAppPreferences(); //TODO: We do this in a lot of places, we should wrap it somewhere if(prefs.contains(USER_DOMAIN_SUFFIX)) { domainedUsername += "@" + prefs.getString(USER_DOMAIN_SUFFIX,null); } this.credentials = new UsernamePasswordCredentials(domainedUsername, password); passwordAuthentication = new PasswordAuthentication (domainedUsername, password.toCharArray()); this.username = username; } public HttpRequestGenerator() { //No authentication possible } public HttpResponse makeCaseFetchRequest(String baseUri) throws ClientProtocolException, IOException { return makeCaseFetchRequest(baseUri, true); } public HttpResponse get(String uri) throws ClientProtocolException, IOException { HttpClient client = client(); System.out.println("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.toString() + " to " + newGetUri); request.abort(); //Make a new response to the redirect request = new HttpGet(newGetUri); addHeaders(request, ""); response = execute(client, request); } return response; } 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(); } 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(); String uri = serverUri.toString(); System.out.println("Fetching from: " + uri); HttpGet request = new HttpGet(uri); addHeaders(request, syncToken); return execute(client, request); } 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(); } 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", "1.0"); if(lastToken != null) { base.addHeader("X-CommCareHQ-LastSyncToken",lastToken); } base.addHeader("x-openrosa-deviceid", CommCareApplication._().getPhoneId()); } public String getSyncToken(String username) { if(username == null) { return null; } SqlStorage<User> storage = CommCareApplication._().getUserStorage(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 String getDigest() { return CaseDBUtils.computeHash(CommCareApplication._().getUserStorage(ACase.STORAGE_KEY, ACase.class)); } 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(); } 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, GlobalConstants.CONNECTION_TIMEOUT); HttpConnectionParams.setSoTimeout(params, GlobalConstants.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 * * @param client * @param request * @return */ 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 it is, verify that we're on the same server. if(url.getHost().equals(newUrl.getHost())) { return true; } else { //otherwise we got redirected from a secure link to a different //link, which isn't acceptable for now. return false; } } /** * TODO: At some point in the future this kind of division will be more central * but this generates an input stream for a URL using the best package for your * application * * @param url * @return a Stream to that URL */ public InputStream simpleGet(URL url) throws IOException { // only for versions past gingerbread use the HttpURLConnection if (android.os.Build.VERSION.SDK_INT > 11) { if(passwordAuthentication != null) { Authenticator.setDefault(new Authenticator() { /* * (non-Javadoc) * @see java.net.Authenticator#getPasswordAuthentication() */ @Override protected PasswordAuthentication getPasswordAuthentication() { return passwordAuthentication; } }); } int responseCode =-1; HttpURLConnection con = (HttpURLConnection) url.openConnection(); setup(con); // Start the query con.connect(); try { responseCode = con.getResponseCode(); //It's possible we're getting redirected from http to https //if so, we need to handle it explicitly if(responseCode == 301) { //only allow one level of redirection here for now. Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Attempting 1 stage redirect from " + url.toString() + " to " + con.getURL().toString()); URL newUrl = con.getURL(); con.disconnect(); con = (HttpURLConnection) newUrl.openConnection(); setup(con); con.connect(); } //Don't allow redirects _from_ https _to_ https unless they are redirecting to the same server. if(!HttpRequestGenerator.isValidRedirect(url, con.getURL())) { Logger.log(AndroidLogger.TYPE_WARNING_NETWORK, "Invalid redirect from " + url.toString() + " to " + con.getURL().toString()); throw new IOException("Invalid redirect from secure server to insecure server"); } return con.getInputStream(); } catch(IOException e) { if(e.getMessage().toLowerCase().contains("authentication") || responseCode == 401) { //Android http libraries _suuuuuck_, let's try apache. } else { throw e; } } } //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(); } /** * @param con * @throws IOException */ private void setup(HttpURLConnection con) throws IOException { con.setConnectTimeout(GlobalConstants.CONNECTION_TIMEOUT); con.setReadTimeout(GlobalConstants.CONNECTION_SO_TIMEOUT); con.setRequestMethod("GET"); con.setDoInput(true); con.setInstanceFollowRedirects(true); } }