/* * Copyright (C) 2011 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.odk.collect.android.utilities; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.TimeZone; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; 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.CredentialsProvider; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.params.AuthPolicy; import org.apache.http.client.params.ClientPNames; import org.apache.http.client.params.HttpClientParams; import org.apache.http.client.protocol.ClientContext; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.protocol.HttpContext; import org.kxml2.io.KXmlParser; import org.kxml2.kdom.Document; import org.odk.collect.android.application.Collect; import org.xmlpull.v1.XmlPullParser; import android.text.format.DateFormat; import android.util.Log; /** * Common utility methods for managing the credentials associated with the request context and * constructing http context, client and request with the proper parameters and OpenRosa headers. * * @author mitchellsundt@gmail.com */ public final class WebUtils { public static final String t = "WebUtils"; public static final String OPEN_ROSA_VERSION_HEADER = "X-OpenRosa-Version"; public static final String OPEN_ROSA_VERSION = "1.0"; private static final String DATE_HEADER = "Date"; public static final String HTTP_CONTENT_TYPE_TEXT_XML = "text/xml"; public static final int CONNECTION_TIMEOUT = 30000; private static final GregorianCalendar g = new GregorianCalendar(TimeZone.getTimeZone("GMT")); public static final List<AuthScope> buildAuthScopes(String host) { List<AuthScope> asList = new ArrayList<AuthScope>(); AuthScope a; // allow digest auth on any port... a = new AuthScope(host, -1, null, AuthPolicy.DIGEST); asList.add(a); // and allow basic auth on the standard TLS/SSL ports... a = new AuthScope(host, 443, null, AuthPolicy.BASIC); asList.add(a); a = new AuthScope(host, 8443, null, AuthPolicy.BASIC); asList.add(a); return asList; } public static final void clearAllCredentials() { HttpContext localContext = Collect.getInstance().getHttpContext(); CredentialsProvider credsProvider = (CredentialsProvider) localContext.getAttribute(ClientContext.CREDS_PROVIDER); credsProvider.clear(); } public static final boolean hasCredentials(String userEmail, String host) { HttpContext localContext = Collect.getInstance().getHttpContext(); CredentialsProvider credsProvider = (CredentialsProvider) localContext.getAttribute(ClientContext.CREDS_PROVIDER); List<AuthScope> asList = buildAuthScopes(host); boolean hasCreds = true; for (AuthScope a : asList) { Credentials c = credsProvider.getCredentials(a); if (c == null) { hasCreds = false; continue; } } return hasCreds; } public static final void addCredentials(String userEmail, String password, String host) { HttpContext localContext = Collect.getInstance().getHttpContext(); Credentials c = new UsernamePasswordCredentials(userEmail, password); addCredentials(localContext, c, host); } private static final void addCredentials(HttpContext localContext, Credentials c, String host) { CredentialsProvider credsProvider = (CredentialsProvider) localContext.getAttribute(ClientContext.CREDS_PROVIDER); List<AuthScope> asList = buildAuthScopes(host); for (AuthScope a : asList) { credsProvider.setCredentials(a, c); } } private static final void setOpenRosaHeaders(HttpRequest req) { req.setHeader(OPEN_ROSA_VERSION_HEADER, OPEN_ROSA_VERSION); g.setTime(new Date()); req.setHeader(DATE_HEADER, DateFormat.format("E, dd MMM yyyy hh:mm:ss zz", g).toString()); } public static final HttpHead createOpenRosaHttpHead(URI uri) { HttpHead req = new HttpHead(uri); setOpenRosaHeaders(req); return req; } public static final HttpGet createOpenRosaHttpGet(URI uri) { return createOpenRosaHttpGet(uri, ""); } public static final HttpGet createOpenRosaHttpGet(URI uri, String auth) { HttpGet req = new HttpGet(); setOpenRosaHeaders(req); setGoogleHeaders(req, auth); req.setURI(uri); return req; } public static final void setGoogleHeaders(HttpRequest req, String auth) { if ((auth != null) && (auth.length() > 0)) { req.setHeader("Authorization", "GoogleLogin auth=" + auth); } } public static final HttpPost createOpenRosaHttpPost(URI uri) { return createOpenRosaHttpPost(uri, ""); } public static final HttpPost createOpenRosaHttpPost(URI uri, String auth) { HttpPost req = new HttpPost(uri); setOpenRosaHeaders(req); setGoogleHeaders(req, auth); return req; } public static final HttpClient createHttpClient(int timeout) { // configure connection HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, timeout); HttpConnectionParams.setSoTimeout(params, 2*timeout); // support redirecting to handle http: => https: transition HttpClientParams.setRedirecting(params, true); // support authenticating HttpClientParams.setAuthenticating(params, true); // if possible, bias toward digest auth (may not be in 4.0 beta 2) List<String> authPref = new ArrayList<String>(); authPref.add(AuthPolicy.DIGEST); authPref.add(AuthPolicy.BASIC); // does this work in Google's 4.0 beta 2 snapshot? params.setParameter("http.auth-target.scheme-pref", authPref); // setup client HttpClient httpclient = new EnhancedHttpClient(params); httpclient.getParams().setParameter(ClientPNames.MAX_REDIRECTS, 1); httpclient.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true); return httpclient; } /** * Common method for returning a parsed xml document given a url and the http context and client * objects involved in the web connection. * * @param urlString * @param localContext * @param httpclient * @return */ public static DocumentFetchResult getXmlDocument(String urlString, HttpContext localContext, HttpClient httpclient, String auth) { URI u = null; try { URL url = new URL(URLDecoder.decode(urlString, "utf-8")); u = url.toURI(); } catch (Exception e) { e.printStackTrace(); return new DocumentFetchResult(e.getLocalizedMessage() // + app.getString(R.string.while_accessing) + urlString); + ("while accessing") + urlString, 0); } // set up request... HttpGet req = WebUtils.createOpenRosaHttpGet(u, auth); HttpResponse response = null; try { response = httpclient.execute(req, localContext); int statusCode = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); if (entity != null && (statusCode != 200 || !entity.getContentType().getValue().toLowerCase() .contains(WebUtils.HTTP_CONTENT_TYPE_TEXT_XML))) { try { // don't really care about the stream... InputStream is = response.getEntity().getContent(); // read to end of stream... final long count = 1024L; while (is.skip(count) == count) ; is.close(); } catch (Exception e) { e.printStackTrace(); } } if (statusCode != 200) { String webError = response.getStatusLine().getReasonPhrase() + " (" + statusCode + ")"; return new DocumentFetchResult(u.toString() + " responded with: " + webError, statusCode); } if (entity == null) { String error = "No entity body returned from: " + u.toString(); Log.e(t, error); return new DocumentFetchResult(error, 0); } if (!entity.getContentType().getValue().toLowerCase() .contains(WebUtils.HTTP_CONTENT_TYPE_TEXT_XML)) { String error = "ContentType: " + entity.getContentType().getValue() + " returned from: " + u.toString() + " is not text/xml. This is often caused a network proxy. Do you need to login to your network?"; Log.e(t, error); return new DocumentFetchResult(error, 0); } // parse response Document doc = null; try { InputStream is = null; InputStreamReader isr = null; try { is = entity.getContent(); isr = new InputStreamReader(is, "UTF-8"); doc = new Document(); KXmlParser parser = new KXmlParser(); parser.setInput(isr); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); doc.parse(parser); isr.close(); } finally { if (isr != null) { try { isr.close(); } catch (Exception e) { // no-op } } if (is != null) { try { is.close(); } catch (Exception e) { // no-op } } } } catch (Exception e) { e.printStackTrace(); String error = "Parsing failed with " + e.getMessage() + "while accessing " + u.toString(); Log.e(t, error); return new DocumentFetchResult(error, 0); } boolean isOR = false; Header[] fields = response.getHeaders(WebUtils.OPEN_ROSA_VERSION_HEADER); if (fields != null && fields.length >= 1) { isOR = true; boolean versionMatch = false; boolean first = true; StringBuilder b = new StringBuilder(); for (Header h : fields) { if (WebUtils.OPEN_ROSA_VERSION.equals(h.getValue())) { versionMatch = true; break; } if (!first) { b.append("; "); } first = false; b.append(h.getValue()); } if (!versionMatch) { Log.w( t, WebUtils.OPEN_ROSA_VERSION_HEADER + " unrecognized version(s): " + b.toString()); } } return new DocumentFetchResult(doc, isOR); } catch (Exception e) { e.printStackTrace(); String cause; if (e.getCause() != null) { cause = e.getCause().getMessage(); } else { cause = e.getMessage(); } String error = "Error: " + cause + " while accessing " + u.toString(); Log.w(t, error); return new DocumentFetchResult(error, 0); } } }