1
0
mirror of https://github.com/etesync/android synced 2025-02-19 19:12:09 +00:00

Handle redirections to relative URLs correctly (see #282) + tests; minor GUI change

This commit is contained in:
rfc2822 2014-07-20 14:29:52 +02:00
parent 1cde020619
commit d712238700
10 changed files with 193 additions and 57 deletions

View File

@ -26,7 +26,8 @@
<EditText <EditText
android:id="@+id/baseURL" android:id="@+id/baseURL"
android:layout_gravity="fill_horizontal" android:layout_gravity="fill_horizontal"
android:hint="myserver/dav/" android:hint="my.server.com"
android:imeOptions="flagForceAscii|actionNext"
android:inputType="textUri" android:inputType="textUri"
android:layout_width="0dp" android:layout_width="0dp"
android:scrollHorizontally="true" android:scrollHorizontally="true"
@ -52,6 +53,7 @@
android:id="@+id/userName" android:id="@+id/userName"
android:layout_gravity="fill_horizontal" android:layout_gravity="fill_horizontal"
android:inputType="textNoSuggestions|textEmailAddress" android:inputType="textNoSuggestions|textEmailAddress"
android:imeOptions="actionNext"
android:layout_width="0dp" android:layout_width="0dp"
android:scrollHorizontally="true" android:scrollHorizontally="true"
android:scrollbars="horizontal" android:scrollbars="horizontal"
@ -65,6 +67,7 @@
android:id="@+id/password" android:id="@+id/password"
android:layout_gravity="fill_horizontal" android:layout_gravity="fill_horizontal"
android:inputType="textPassword" android:inputType="textPassword"
android:imeOptions="actionGo"
android:layout_width="0dp" android:layout_width="0dp"
android:scrollHorizontally="true" android:scrollHorizontally="true"
android:scrollbars="horizontal" android:scrollbars="horizontal"

View File

@ -5,6 +5,7 @@ import java.net.URISyntaxException;
import android.util.Log; import android.util.Log;
import ch.boye.httpclientandroidlib.Header; import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpHost;
import ch.boye.httpclientandroidlib.HttpRequest; import ch.boye.httpclientandroidlib.HttpRequest;
import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.ProtocolException; import ch.boye.httpclientandroidlib.ProtocolException;
@ -13,6 +14,7 @@ import ch.boye.httpclientandroidlib.client.RedirectStrategy;
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
import ch.boye.httpclientandroidlib.client.methods.RequestBuilder; import ch.boye.httpclientandroidlib.client.methods.RequestBuilder;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext; import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.client.utils.URIUtils;
import ch.boye.httpclientandroidlib.protocol.HttpContext; import ch.boye.httpclientandroidlib.protocol.HttpContext;
/** /**
@ -75,13 +77,15 @@ public class DavRedirectStrategy implements RedirectStrategy {
// some servers don't return absolute URLs as required by RFC 2616 // some servers don't return absolute URLs as required by RFC 2616
if (!location.isAbsolute()) { if (!location.isAbsolute()) {
Log.w(TAG, "Received invalid redirection with relative URL, repairing"); Log.w(TAG, "Received invalid redirection to relative URL, repairing");
URI originalURI = new URI(request.getRequestLine().getUri());
// determine original URL if (!originalURI.isAbsolute()) {
final HttpClientContext clientContext = HttpClientContext.adapt(context); final HttpHost target = HttpClientContext.adapt(context).getTargetHost();
final URI originalURI = new URI(clientContext.getTargetHost() + request.getRequestLine().getUri()); if (target != null)
originalURI = URIUtils.rewriteURI(originalURI, target);
// determine new location relative to original URL else
return null;
}
location = originalURI.resolve(location); location = originalURI.resolve(location);
} }
return location; return location;

View File

@ -55,6 +55,10 @@ import ch.boye.httpclientandroidlib.message.BasicLineParser;
import ch.boye.httpclientandroidlib.util.EntityUtils; import ch.boye.httpclientandroidlib.util.EntityUtils;
/**
* Represents a WebDAV resource (file or collection).
* This class is used for all CalDAV/CardDAV communcation.
*/
@ToString @ToString
public class WebDavResource { public class WebDavResource {
private static final String TAG = "davdroid.WebDavResource"; private static final String TAG = "davdroid.WebDavResource";
@ -119,9 +123,10 @@ public class WebDavResource {
} }
} }
private WebDavResource(WebDavResource parent) { // based on existing WebDavResource, reuse settings WebDavResource(WebDavResource parent) { // copy constructor: based on existing WebDavResource, reuse settings
httpClient = parent.httpClient; httpClient = parent.httpClient;
context = parent.context; context = parent.context;
location = parent.location;
} }
protected WebDavResource(WebDavResource parent, URI uri) { protected WebDavResource(WebDavResource parent, URI uri) {

View File

@ -1,6 +1,6 @@
{"plugins":[ {"plugins":[
"assets", "assets",
"redirect", "redirect",
"dav-default", "dav",
"dav-invalid" "dav-invalid"
]} ]}

View File

@ -1,10 +1,25 @@
var RoboHydraHead = require("robohydra").heads.RoboHydraHead; require('../simple');
var RoboHydraHead = require('robohydra').heads.RoboHydraHead;
exports.getBodyParts = function(conf) { exports.getBodyParts = function(conf) {
return { return {
heads: [ heads: [
// well-known URIs
new SimpleResponseHead({
path: '/.well-known/caldav',
status: 302,
headers: { Location: '/dav/' }
}),
new SimpleResponseHead({
path: '/.well-known/caldav',
status: 302,
headers: { Location: '/dav/' }
}),
// generic redirections
new RoboHydraHead({ new RoboHydraHead({
path: "/redirect/301", path: '/redirect/301',
handler: function(req,res,next) { handler: function(req,res,next) {
res.statusCode = 301; res.statusCode = 301;
var location = req.queryParams['to'] || '/assets/test.random'; var location = req.queryParams['to'] || '/assets/test.random';
@ -15,7 +30,7 @@ exports.getBodyParts = function(conf) {
} }
}), }),
new RoboHydraHead({ new RoboHydraHead({
path: "/redirect/302", path: '/redirect/302',
handler: function(req,res,next) { handler: function(req,res,next) {
res.statusCode = 302; res.statusCode = 302;
var location = req.queryParams['to'] || '/assets/test.random'; var location = req.queryParams['to'] || '/assets/test.random';
@ -24,7 +39,19 @@ exports.getBodyParts = function(conf) {
} }
res.end(); res.end();
} }
}),
// special redirections
new SimpleResponseHead({
path: '/redirect/relative',
status: 302,
headers: { Location: '/new/location' }
}),
new SimpleResponseHead({
path: '/redirect/without-location',
status: 302
}) })
] ]
}; };
}; };

View File

@ -0,0 +1,28 @@
var roboHydra = require("robohydra"),
roboHydraHeads = roboHydra.heads,
roboHydraHead = roboHydraHeads.RoboHydraHead;
SimpleResponseHead = roboHydraHeads.roboHydraHeadType({
name: 'Simple HTTP Response',
mandatoryProperties: [ 'path', 'status' ],
optionalProperties: [ 'headers', 'body' ],
parentPropBuilder: function() {
var head = this;
return {
path: this.path,
handler: function(req,res,next) {
res.statusCode = head.status;
if (typeof head.headers != 'undefined')
res.headers = head.headers;
if (typeof head.body != 'undefined')
res.write(head.body);
else
res.write();
res.end();
}
}
}
});
module.exports = SimpleResponseHead;

View File

@ -0,0 +1,19 @@
package at.bitfire.davdroid.test;
import java.net.URI;
import java.net.URISyntaxException;
import android.util.Log;
public class Constants {
public static final String ROBOHYDRA_BASE = "http://10.0.0.11:3000/";
public static URI roboHydra;
static {
try {
roboHydra = new URI(ROBOHYDRA_BASE);
} catch(URISyntaxException e) {
Log.wtf("davdroid.test.Constants", "Invalid RoboHydra base URL");
}
}
}

View File

@ -0,0 +1,73 @@
package at.bitfire.davdroid.webdav;
import java.io.IOException;
import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.test.Constants;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.methods.HttpOptions;
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
import ch.boye.httpclientandroidlib.protocol.HttpContext;
public class DavRedirectStrategyTest extends InstrumentationTestCase {
CloseableHttpClient httpClient;
DavRedirectStrategy strategy = DavRedirectStrategy.INSTANCE;
@Override
protected void setUp() {
httpClient = HttpClientBuilder.create()
.useSystemProperties()
.disableRedirectHandling()
.build();
}
@Override
protected void tearDown() throws IOException {
httpClient.close();
}
// happy cases
public void testNonRedirection() throws Exception {
HttpUriRequest request = new HttpOptions(Constants.roboHydra);
HttpResponse response = httpClient.execute(request);
assertFalse(strategy.isRedirected(request, response, null));
}
public void testDefaultRedirection() throws Exception {
final String newLocation = "/new-location";
HttpContext context = HttpClientContext.create();
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/301?to=" + newLocation));
HttpResponse response = httpClient.execute(request, context);
assertTrue(strategy.isRedirected(request, response, context));
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
assertEquals(Constants.roboHydra.resolve(newLocation), redirected.getURI());
}
// error cases
public void testMissingLocation() throws Exception {
HttpContext context = HttpClientContext.create();
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/without-location"));
HttpResponse response = httpClient.execute(request, context);
assertFalse(strategy.isRedirected(request, response, context));
}
public void testRelativeLocation() throws Exception {
HttpContext context = HttpClientContext.create();
HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/relative"));
HttpResponse response = httpClient.execute(request, context);
assertTrue(strategy.isRedirected(request, response, context));
HttpUriRequest redirected = strategy.getRedirect(request, response, context);
assertEquals(Constants.roboHydra.resolve("/new/location"), redirected.getURI());
}
}

View File

@ -5,7 +5,7 @@
* which accompanies this distribution, and is available at * which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html * http://www.gnu.org/licenses/gpl.html
******************************************************************************/ ******************************************************************************/
package at.bitfire.davdroid.webdav.test; package at.bitfire.davdroid.webdav;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -22,6 +22,7 @@ import org.apache.commons.io.IOUtils;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import at.bitfire.davdroid.test.Constants;
import at.bitfire.davdroid.webdav.DavException; import at.bitfire.davdroid.webdav.DavException;
import at.bitfire.davdroid.webdav.DavHttpClient; import at.bitfire.davdroid.webdav.DavHttpClient;
import at.bitfire.davdroid.webdav.DavMultiget; import at.bitfire.davdroid.webdav.DavMultiget;
@ -37,12 +38,14 @@ import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
// tests require running robohydra! // tests require running robohydra!
public class WebDavResourceTest extends InstrumentationTestCase { public class WebDavResourceTest extends InstrumentationTestCase {
static final String ROBOHYDRA_BASE = "http://10.0.0.11:3000/";
static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 }; static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 };
final static String PATH_SIMPLE_FILE = "collection/new.file";
AssetManager assetMgr; AssetManager assetMgr;
CloseableHttpClient httpClient; CloseableHttpClient httpClient;
WebDavResource baseDAV;
WebDavResource simpleFile, WebDavResource simpleFile,
davCollection, davNonExistingFile, davExistingFile, davCollection, davNonExistingFile, davExistingFile,
davInvalid; davInvalid;
@ -53,57 +56,35 @@ public class WebDavResourceTest extends InstrumentationTestCase {
assetMgr = getInstrumentation().getContext().getResources().getAssets(); assetMgr = getInstrumentation().getContext().getResources().getAssets();
simpleFile = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "assets/test.random")); baseDAV = new WebDavResource(httpClient, Constants.roboHydra.resolve("/dav/"));
davCollection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "dav/")); simpleFile = new WebDavResource(httpClient, new URI(Constants.ROBOHYDRA_BASE + "assets/test.random"));
davCollection = new WebDavResource(httpClient, new URI(Constants.ROBOHYDRA_BASE + "dav/"));
davNonExistingFile = new WebDavResource(davCollection, "collection/new.file"); davNonExistingFile = new WebDavResource(davCollection, "collection/new.file");
davExistingFile = new WebDavResource(davCollection, "collection/existing.file"); davExistingFile = new WebDavResource(davCollection, "collection/existing.file");
davInvalid = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "dav-invalid/")); davInvalid = new WebDavResource(httpClient, new URI(Constants.ROBOHYDRA_BASE + "dav-invalid/"));
} }
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws IOException {
httpClient.close(); httpClient.close();
} }
/* test resource name handling */
public void testGetName() {
// collection names should have a trailing slash
assertEquals("dav", davCollection.getName());
// but non-collection names shouldn't
assertEquals("test.random", simpleFile.getName());
}
public void testTrailingSlash() throws URISyntaxException {
// collections should have a trailing slash
assertEquals("/dav/", davCollection.getLocation().getPath());
// but non-collection members shouldn't
assertEquals("/assets/test.random", simpleFile.getLocation().getPath());
}
/* test feature detection */ /* test feature detection */
public void testOptions() throws URISyntaxException, IOException, HttpException { public void testOptions() throws URISyntaxException, IOException, HttpException {
String[] davMethods = new String[] { "PROPFIND", "GET", "PUT", "DELETE" }, String[] davMethods = new String[] { "PROPFIND", "GET", "PUT", "DELETE", "REPORT" },
davCapabilities = new String[] { "addressbook", "calendar-access" }; davCapabilities = new String[] { "addressbook", "calendar-access" };
// server without DAV WebDavResource capable = new WebDavResource(baseDAV);
simpleFile.options(); capable.options();
for (String method : davMethods)
assertFalse(simpleFile.supportsMethod(method));
for (String capability : davCapabilities)
assertFalse(simpleFile.supportsDAV(capability));
// server with DAV
davCollection.options();
for (String davMethod : davMethods) for (String davMethod : davMethods)
assert(davCollection.supportsMethod(davMethod)); assert(capable.supportsMethod(davMethod));
for (String capability : davCapabilities) for (String capability : davCapabilities)
assert(davCollection.supportsDAV(capability)); assert(capable.supportsDAV(capability));
} }
public void testPropfindCurrentUserPrincipal() throws IOException, HttpException, DavException { public void testPropfindCurrentUserPrincipal() throws IOException, HttpException, DavException {
@ -156,15 +137,11 @@ public class WebDavResourceTest extends InstrumentationTestCase {
/* test normal HTTP/WebDAV */ /* test normal HTTP/WebDAV */
public void testRedirections() throws URISyntaxException, IOException, DavException, HttpException { public void testPropfindRedirection() throws URISyntaxException, IOException, DavException, HttpException {
// PROPFIND redirection // PROPFIND redirection
WebDavResource redirection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "redirect/301?to=/dav/")); WebDavResource redirected = new WebDavResource(baseDAV, "/redirect/301?to=/dav/");
redirection.propfind(Mode.CURRENT_USER_PRINCIPAL); redirected.propfind(Mode.CURRENT_USER_PRINCIPAL);
assertEquals("/dav/", redirection.getLocation().getPath()); assertEquals("/dav/", redirected.getLocation().getPath());
// normal GET redirection
redirection = new WebDavResource(httpClient, new URI(ROBOHYDRA_BASE + "redirect/301"));
redirection.get();
} }
public void testGet() throws URISyntaxException, IOException, HttpException, DavException { public void testGet() throws URISyntaxException, IOException, HttpException, DavException {
@ -243,7 +220,7 @@ public class WebDavResourceTest extends InstrumentationTestCase {
dav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS); dav.propfind(HttpPropfind.Mode.MEMBERS_COLLECTIONS);
List<WebDavResource> members = dav.getMembers(); List<WebDavResource> members = dav.getMembers();
assertEquals(2, members.size()); assertEquals(2, members.size());
assertEquals(ROBOHYDRA_BASE + "dav/addressbooks/user%40domain/My%20Contacts%3a1.vcf/", members.get(0).getLocation().toString()); assertEquals(Constants.ROBOHYDRA_BASE + "dav/addressbooks/user%40domain/My%20Contacts%3a1.vcf/", members.get(0).getLocation().toString());
assertEquals("HTTPS://example.com/user%40domain/absolute-url.vcf", members.get(1).getLocation().toString()); assertEquals("HTTPS://example.com/user%40domain/absolute-url.vcf", members.get(1).getLocation().toString());
} }