1
0
mirror of https://github.com/etesync/android synced 2024-11-22 16:08:13 +00:00

Handle HTTP redirections (fixes #83)

This commit is contained in:
rfc2822 2014-07-18 19:04:27 +02:00
parent 1678873885
commit cf40cb2ebc
12 changed files with 247 additions and 83 deletions

View File

@ -88,7 +88,7 @@
o comprar-lo.</p>
<h1>Llicència</h1>
<p>Copyright (c) 2013 2014 Richard Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>). Tots els drets reservats.
<p>Copyright (c) 2013 2014 Ricki Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>). Tots els drets reservats.
Aquest programa i tots els materials que l\'acompanyen estan disponibles sota els termes de la GNU Public License v3.0 que acompanya
aquesta distribució i està disponible a <a
href="http://www.gnu.org/licenses/gpl.html">http://www.gnu.org/licenses/gpl.html</a>. Respecte al Google Play, Samsung

View File

@ -82,7 +82,7 @@
<a href="http://davdroid.bitfire.at/donate?pk_campaign=davdroid-app&amp;pk_kwd=main-activity">für DAVdroid spenden</a> oder die App kaufen.</p>
<h1>Lizenz</h1>
<p>Copyright (c) 2013 2014 Richard Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>), alle Rechte
<p>Copyright (c) 2013 2014 Ricki Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>), alle Rechte
vorbehalten. Dieses Programm ist freie Software. Sie können es unter den Bedingungen der <a href="http://www.gnu.org/licenses/gpl.html">GNU
General Public License Version 3</a>, wie von der Free Software Foundation veröffentlicht, weitergeben und/oder modifizieren.
Sofern Google Play oder Samsung Store andere Bedingungen benötigen, gelten für über den jeweiligen Markt heruntergeladene

View File

@ -65,7 +65,7 @@
Si vous voulez aider ce projet <a href="http://davdroid.bitfire.at/donate?pk_campaign=davdroid-app&amp;pk_kwd=main-activity">faites un don à DAVdroid</a> ou achetez le</p>
<h1>License</h1>
<p>Copyright (c) 2013 2014 Richard Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>). All rights reserved.
<p>Copyright (c) 2013 2014 Ricki Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>). All rights reserved.
Ce programme et les documents qui l\'accompagnent sont mis à disposition sous les termes de la Licence Public GNU v3.0 qui
accompagne cette distribution, et est disponible à <a
href="http://www.gnu.org/licenses/gpl.html">http://www.gnu.org/licenses/gpl.html</a>. En ce qui concerne Google Play ou Samsung Store, les conditions respectives s\'appliquent pour les versions qui sont téléchargées via ces services.</p>

View File

@ -87,7 +87,7 @@
or purchasing it.</p>
<h1>License</h1>
<p>Copyright (c) 2013 2014 Richard Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>). All rights reserved.
<p>Copyright (c) 2013 2014 Ricki Hirner (<a href="http://www.bitfire.at">bitfire web engineering</a>). All rights reserved.
This program and the accompanying materials are made available under the terms of the GNU Public License v3.0 which
accompanies this distribution, and is available at <a
href="http://www.gnu.org/licenses/gpl.html">http://www.gnu.org/licenses/gpl.html</a>. As far as Google Play, Samsung

View File

@ -48,7 +48,7 @@ public abstract class RemoteCollection<T extends Resource> {
public RemoteCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
this.httpClient = httpClient;
collection = new WebDavResource(httpClient, new URI(baseURL), user, password, preemptiveAuth, true);
collection = new WebDavResource(httpClient, new URI(baseURL), user, password, preemptiveAuth);
}

View File

@ -125,14 +125,15 @@ public class EnterCredentialsFragment extends Fragment implements TextWatcher {
editUserName.getText().length() > 0 &&
editPassword.getText().length() > 0;
// check host name
try {
URI uri = new URI(URIUtils.sanitize(protocol + editBaseURL.getText().toString()));
if (StringUtils.isBlank(uri.getHost()))
if (ok)
// check host name
try {
URI uri = new URI(URIUtils.sanitize(protocol + editBaseURL.getText().toString()));
if (StringUtils.isBlank(uri.getHost()))
ok = false;
} catch (URISyntaxException e) {
ok = false;
} catch (URISyntaxException e) {
ok = false;
}
}
MenuItem item = menu.findItem(R.id.next);
item.setEnabled(ok);

View File

@ -117,7 +117,7 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC
try {
// (1/5) detect capabilities
WebDavResource base = new WebDavResource(httpClient, new URI(serverInfo.getProvidedURL()), serverInfo.getUserName(),
serverInfo.getPassword(), serverInfo.isAuthPreemptive(), true);
serverInfo.getPassword(), serverInfo.isAuthPreemptive());
base.options();
serverInfo.setCardDAV(base.supportsDAV("addressbook"));

View File

@ -52,6 +52,7 @@ public class DavHttpClient {
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(defaultRqConfig)
.setRetryHandler(DavHttpRequestRetryHandler.INSTANCE)
.setRedirectStrategy(DavRedirectStrategy.INSTANCE)
.setUserAgent("DAVdroid/" + Constants.APP_VERSION)
.disableCookieManagement();

View File

@ -0,0 +1,94 @@
package at.bitfire.davdroid.webdav;
import java.net.URI;
import java.net.URISyntaxException;
import android.util.Log;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.ProtocolException;
import ch.boye.httpclientandroidlib.RequestLine;
import ch.boye.httpclientandroidlib.client.RedirectStrategy;
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
import ch.boye.httpclientandroidlib.client.methods.RequestBuilder;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.protocol.HttpContext;
/**
* Custom Redirect Strategy that handles 30x for CalDAV/CardDAV-specific requests correctly
*/
public class DavRedirectStrategy implements RedirectStrategy {
private final static String TAG = "davdroid.DavRedirectStrategy";
final static DavRedirectStrategy INSTANCE = new DavRedirectStrategy();
protected final static String REDIRECTABLE_METHODS[] = {
"OPTIONS", "GET", "PUT", "DELETE"
};
@Override
public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
RequestLine line = request.getRequestLine();
String location = getLocation(request, response, context).toString();
Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location);
return RequestBuilder.copy(request)
.setUri(location)
.removeHeaders("Content-Length") // Content-Length will be set again automatically, if required;
// remove it now to avoid duplicate header
.build();
}
/**
* Determines whether a response indicates a redirection and if it does, whether to follow this redirection.
* PROPFIND and REPORT must handle redirections explicitely because multi-status processing requires knowledge of the content location.
* @return true for 3xx responses on OPTIONS, GET, PUT, DELETE requests that have a valid Location header; false otherwise
*/
@Override
public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException {
if (response.getStatusLine().getStatusCode()/100 == 3) {
boolean redirectable = false;
for (String method : REDIRECTABLE_METHODS)
if (method.equalsIgnoreCase(request.getRequestLine().getMethod())) {
redirectable = true;
break;
}
return redirectable && getLocation(request, response, context) != null;
}
return false;
}
/**
* Gets the destination of a redirection
* @return absolute URL of new location; null if not available
*/
static URI getLocation(HttpRequest request, HttpResponse response, HttpContext context) {
Header locationHdr = response.getFirstHeader("Location");
if (locationHdr == null) {
Log.e(TAG, "Received redirection without Location header, ignoring");
return null;
}
try {
URI location = new URI(locationHdr.getValue());
// some servers don't return absolute URLs as required by RFC 2616
if (!location.isAbsolute()) {
Log.w(TAG, "Received invalid redirection with relative URL, repairing");
// determine original URL
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final URI originalURI = new URI(clientContext.getTargetHost() + request.getRequestLine().getUri());
// determine new location relative to original URL
location = originalURI.resolve(location);
}
return location;
} catch (URISyntaxException e) {
Log.e(TAG, "Received redirection from/to invalid URL, ignoring", e);
}
return null;
}
}

View File

@ -95,19 +95,16 @@ public class WebDavResource {
protected HttpClientContext context;
public WebDavResource(CloseableHttpClient httpClient, URI baseURL, boolean trailingSlash) throws URISyntaxException {
public WebDavResource(CloseableHttpClient httpClient, URI baseURL) throws URISyntaxException {
this.httpClient = httpClient;
location = baseURL.normalize();
if (trailingSlash && !location.getRawPath().endsWith("/"))
location = new URI(location.getScheme(), location.getSchemeSpecificPart() + "/", null);
context = HttpClientContext.create();
context.setCredentialsProvider(new BasicCredentialsProvider());
}
public WebDavResource(CloseableHttpClient httpClient, URI baseURL, String username, String password, boolean preemptive, boolean trailingSlash) throws URISyntaxException {
this(httpClient, baseURL, trailingSlash);
public WebDavResource(CloseableHttpClient httpClient, URI baseURL, String username, String password, boolean preemptive) throws URISyntaxException {
this(httpClient, baseURL);
HttpHost host = new HttpHost(baseURL.getHost(), baseURL.getPort(), baseURL.getScheme());
context.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
@ -250,67 +247,75 @@ public class WebDavResource {
/* collection operations */
public void propfind(HttpPropfind.Mode mode) throws IOException, DavException, HttpException {
HttpPropfind propfind = new HttpPropfind(location, mode);
CloseableHttpResponse response = httpClient.execute(propfind, context);
try {
checkResponse(response);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DavNoMultiStatusException();
HttpEntity entity = response.getEntity();
if (entity == null)
throw new DavNoContentException();
@Cleanup InputStream content = entity.getContent();
CloseableHttpResponse response = null;
// processMultiStatus() requires knowledge of the actual content location,
// so we have to handle redirections manually and create a new request for the new location
for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) {
HttpPropfind propfind = new HttpPropfind(location, mode);
response = httpClient.execute(propfind, context);
DavMultistatus multistatus;
try {
Serializer serializer = new Persister();
multistatus = serializer.read(DavMultistatus.class, content, false);
} catch (Exception ex) {
throw new DavException("Couldn't parse Multi-Status response on PROPFIND", ex);
}
processMultiStatus(multistatus);
if (response.getStatusLine().getStatusCode()/100 == 3) {
location = DavRedirectStrategy.getLocation(propfind, response, context);
Log.i(TAG, "Redirection on PROPFIND; trying again at new content URL: " + location);
// don't forget to throw away the unneeded response content
HttpEntity entity = response.getEntity();
if (entity != null) { @Cleanup InputStream content = entity.getContent(); }
} else
break; // answer was NOT a redirection, continue
}
if (response == null)
throw new DavNoContentException();
try {
checkResponse(response); // will also handle Content-Location
processMultiStatus(response);
} finally {
response.close();
}
}
public void multiGet(DavMultiget.Type type, String[] names) throws IOException, DavException, HttpException {
List<String> hrefs = new LinkedList<String>();
for (String name : names)
hrefs.add(location.resolve(name).getRawPath());
DavMultiget multiget = DavMultiget.newRequest(type, hrefs.toArray(new String[0]));
CloseableHttpResponse response = null;
Serializer serializer = new Persister();
StringWriter writer = new StringWriter();
try {
serializer.write(multiget, writer);
} catch (Exception ex) {
Log.e(TAG, "Couldn't create XML multi-get request", ex);
throw new DavException("Couldn't create multi-get request");
}
HttpReport report = new HttpReport(location, writer.toString());
CloseableHttpResponse response = httpClient.execute(report, context);
try {
checkResponse(response);
// processMultiStatus() requires knowledge of the actual content location,
// so we have to handle redirections manually and create a new request for the new location
for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) {
// build multi-get XML request
List<String> hrefs = new LinkedList<String>();
for (String name : names)
hrefs.add(location.resolve(name).getRawPath());
DavMultiget multiget = DavMultiget.newRequest(type, hrefs.toArray(new String[0]));
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DavNoMultiStatusException();
HttpEntity entity = response.getEntity();
if (entity == null)
throw new DavNoContentException();
@Cleanup InputStream content = entity.getContent();
DavMultistatus multiStatus;
StringWriter writer = new StringWriter();
try {
multiStatus = serializer.read(DavMultistatus.class, content, false);
Serializer serializer = new Persister();
serializer.write(multiget, writer);
} catch (Exception ex) {
throw new DavException("Couldn't parse Multi-Status response on REPORT multi-get", ex);
Log.e(TAG, "Couldn't create XML multi-get request", ex);
throw new DavException("Couldn't create multi-get request");
}
processMultiStatus(multiStatus);
// submit REPORT request
HttpReport report = new HttpReport(location, writer.toString());
response = httpClient.execute(report, context);
if (response.getStatusLine().getStatusCode()/100 == 3) {
location = DavRedirectStrategy.getLocation(report, response, context);
Log.i(TAG, "Redirection on REPORT multi-get; trying again at new content URL: " + location);
// don't forget to throw away the unneeded response content
HttpEntity entity = response.getEntity();
if (entity != null) { @Cleanup InputStream content = entity.getContent(); }
} else
break; // answer was NOT a redirection, continue
}
if (response == null)
throw new DavNoContentException();
try {
checkResponse(response); // will also handle Content-Location
processMultiStatus(response);
} finally {
response.close();
}
@ -383,8 +388,19 @@ public class WebDavResource {
/* helpers */
protected static void checkResponse(HttpResponse response) throws HttpException {
protected void checkResponse(HttpResponse response) throws HttpException {
checkResponse(response.getStatusLine());
// handle Content-Location header (see RFC 4918 5.2 Collection Resources)
Header contentLocationHdr = response.getFirstHeader("Content-Location");
if (contentLocationHdr != null)
try {
// Content-Location was set, update location correspondingly
location = location.resolve(new URI(contentLocationHdr.getValue()));
Log.d(TAG, "Set Content-Location to " + location);
} catch (URISyntaxException e) {
Log.w(TAG, "Ignoring invalid Content-Location", e);
}
}
protected static void checkResponse(StatusLine statusLine) throws HttpException {
@ -404,14 +420,30 @@ public class WebDavResource {
}
}
protected void processMultiStatus(DavMultistatus multistatus) throws HttpException, DavException {
if (multistatus.response == null) // empty response
protected void processMultiStatus(HttpResponse response) throws IOException, HttpException, DavException {
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS)
throw new DavNoMultiStatusException();
HttpEntity entity = response.getEntity();
if (entity == null)
throw new DavNoContentException();
@Cleanup InputStream content = entity.getContent();
DavMultistatus multiStatus;
try {
Serializer serializer = new Persister();
multiStatus = serializer.read(DavMultistatus.class, content, false);
} catch (Exception ex) {
throw new DavException("Couldn't parse Multi-Status response on REPORT multi-get", ex);
}
if (multiStatus.response == null) // empty response
throw new DavNoContentException();
// member list will be built from response
List<WebDavResource> members = new LinkedList<WebDavResource>();
for (DavResponse singleResponse : multistatus.response) {
for (DavResponse singleResponse : multiStatus.response) {
URI href;
try {
href = location.resolve(URIUtils.sanitize(singleResponse.getHref().href));
@ -423,12 +455,28 @@ public class WebDavResource {
// about which resource is this response?
WebDavResource referenced = null;
// "this" resource is either at "location"
if (location.equals(href)) { // -> ourselves
referenced = this;
} else {
// or at location + "/" (in case of a collection where the server has implicitly appended the trailing slash)
if (!location.getRawPath().endsWith("/")) // this is only possible if location doesn't have a trailing slash
try {
URI locationAsCollection = new URI(location.getScheme(), location.getAuthority(), location.getPath() + "/", location.getQuery(), null);
if (locationAsCollection.equals(href)) {
Log.d(TAG, "Server implicitly appended trailing slash to " + locationAsCollection);
referenced = this;
}
} catch (URISyntaxException e) {
Log.wtf(TAG, "Couldn't understand our own URI", e);
}
} else { // -> about a member
referenced = new WebDavResource(this, href);
members.add(referenced);
// otherwise, the referenced resource is a member
if (referenced == null) {
referenced = new WebDavResource(this, href);
members.add(referenced);
}
}
for (DavPropstat singlePropstat : singleResponse.getPropstat()) {

View File

@ -4,11 +4,23 @@ exports.getBodyParts = function(conf) {
return {
heads: [
new RoboHydraHead({
path: "/redirect",
path: "/redirect/301",
handler: function(req,res,next) {
res.statusCode = 301;
var location = req.queryParams['to'] || '/assets/test.random';
res.headers = {
Location: location
}
res.end();
}
}),
new RoboHydraHead({
path: "/redirect/302",
handler: function(req,res,next) {
res.statusCode = 302;
var location = req.queryParams['to'] || '/assets/test.random';
res.headers = {
location: 'http://www.example.com'
Location: location
}
res.end();
}

View File

@ -27,6 +27,7 @@ import at.bitfire.davdroid.webdav.DavHttpClient;
import at.bitfire.davdroid.webdav.DavMultiget;
import at.bitfire.davdroid.webdav.HttpException;
import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.HttpPropfind.Mode;
import at.bitfire.davdroid.webdav.NotFoundException;
import at.bitfire.davdroid.webdav.PreconditionFailedException;
import at.bitfire.davdroid.webdav.WebDavResource;
@ -52,13 +53,13 @@ public class WebDavResourceTest extends InstrumentationTestCase {
assetMgr = getInstrumentation().getContext().getResources().getAssets();
simpleFile = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "assets/test.random"), false);
simpleFile = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "assets/test.random"));
davCollection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "dav"), true);
davCollection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "dav/"));
davNonExistingFile = new WebDavResource(davCollection, "collection/new.file");
davExistingFile = new WebDavResource(davCollection, "collection/existing.file");
davInvalid = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "dav-invalid"), true);
davInvalid = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "dav-invalid/"));
}
@Override
@ -112,6 +113,7 @@ public class WebDavResourceTest extends InstrumentationTestCase {
try {
simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL);
fail();
} catch(DavException ex) {
}
assertNull(simpleFile.getCurrentUserPrincipal());
@ -154,8 +156,14 @@ public class WebDavResourceTest extends InstrumentationTestCase {
/* test normal HTTP/WebDAV */
public void testFollowGetRedirections() throws URISyntaxException, IOException, DavException, HttpException {
WebDavResource redirection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "redirect"), false);
public void testRedirections() throws URISyntaxException, IOException, DavException, HttpException {
// PROPFIND redirection
WebDavResource redirection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "redirect/301?to=/dav/"));
redirection.propfind(Mode.CURRENT_USER_PRINCIPAL);
assertEquals("/dav/", redirection.getLocation().getPath());
// normal GET redirection
redirection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "redirect/301"));
redirection.get();
}
@ -167,7 +175,7 @@ public class WebDavResourceTest extends InstrumentationTestCase {
}
public void testGetHttpsWithSni() throws URISyntaxException, HttpException, IOException, DavException {
WebDavResource file = new WebDavResource(httpClient, new URI("https://sni.velox.ch"), false);
WebDavResource file = new WebDavResource(httpClient, new URI("https://sni.velox.ch"));
boolean sniWorking = false;
try {