Repository: ant-ivy Updated Branches: refs/heads/master 41a936d13 -> c0ccd5400
IVY-1336 Add a testcase to make sure that the HTTP handler (backed by the httpclient library) doesn't end in a loop while dealing with 401 errors from the server Project: http://git-wip-us.apache.org/repos/asf/ant-ivy/repo Commit: http://git-wip-us.apache.org/repos/asf/ant-ivy/commit/c0ccd540 Tree: http://git-wip-us.apache.org/repos/asf/ant-ivy/tree/c0ccd540 Diff: http://git-wip-us.apache.org/repos/asf/ant-ivy/diff/c0ccd540 Branch: refs/heads/master Commit: c0ccd54005c0fd2e493c9faa280061b9487c5f96 Parents: 41a936d Author: Jaikiran Pai <jaiki...@apache.org> Authored: Tue Jul 25 10:55:06 2017 +0530 Committer: Jaikiran Pai <jaiki...@apache.org> Committed: Tue Jul 25 11:07:06 2017 +0530 ---------------------------------------------------------------------- .../apache/ivy/util/url/HttpClientHandler.java | 3 +- test/java/org/apache/ivy/TestHelper.java | 58 +++++++++++++++ .../ivy/util/url/HttpclientURLHandlerTest.java | 75 ++++++++++++++++++-- 3 files changed, 129 insertions(+), 7 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ant-ivy/blob/c0ccd540/src/java/org/apache/ivy/util/url/HttpClientHandler.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/ivy/util/url/HttpClientHandler.java b/src/java/org/apache/ivy/util/url/HttpClientHandler.java index 2d4e3fc..e9bc699 100644 --- a/src/java/org/apache/ivy/util/url/HttpClientHandler.java +++ b/src/java/org/apache/ivy/util/url/HttpClientHandler.java @@ -278,7 +278,8 @@ class HttpClientHandler extends AbstractURLHandler implements AutoCloseable { // log and move on Message.debug("Could not close the HTTP response for url=" + sourceURL, e); } - throw new IOException("Response to request '" + httpMethod + " " + sourceURL + "' did not indicate a success (see debug log for details)"); + throw new IOException("Failed response to request '" + httpMethod + " " + sourceURL + "' " + response.getStatusLine().getStatusCode() + + " - '" + response.getStatusLine().getReasonPhrase()); } private Header getContentEncoding(final HttpResponse response) { http://git-wip-us.apache.org/repos/asf/ant-ivy/blob/c0ccd540/test/java/org/apache/ivy/TestHelper.java ---------------------------------------------------------------------- diff --git a/test/java/org/apache/ivy/TestHelper.java b/test/java/org/apache/ivy/TestHelper.java index a908fbd..ebd632a 100644 --- a/test/java/org/apache/ivy/TestHelper.java +++ b/test/java/org/apache/ivy/TestHelper.java @@ -17,6 +17,8 @@ */ package org.apache.ivy; +import com.sun.net.httpserver.BasicAuthenticator; +import com.sun.net.httpserver.HttpContext; import com.sun.net.httpserver.HttpServer; import org.apache.ivy.core.cache.DefaultRepositoryCacheManager; import org.apache.ivy.core.event.EventManager; @@ -38,6 +40,7 @@ import org.apache.ivy.util.FileUtil; import org.apache.tools.ant.DefaultLogger; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.Delete; +import sun.net.httpserver.AuthFilter; import java.io.File; import java.io.IOException; @@ -49,6 +52,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.LinkedHashSet; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -403,4 +407,58 @@ public class TestHelper { } }; } + + /** + * Creates a HTTP server, backed by a local file system, which can be used as a repository to serve Ivy module descriptors + * and artifacts. The context within the server will be backed by {@code BASIC} authentication mechanism with {@code realm} + * as the realm and {@code validCredentials} as the credentials that the server will recognize. The server will allow + * access to resources, only if the credentials that are provided by the request, belong to these credentials. + * <p> + * NOTE: This is supposed to be used only in test cases and only a limited functionality is added in the handler(s) backing the + * server + * + * @param serverAddress The address to which the server will be bound + * @param webAppContext The context root of the application which will be handling the requests to the server + * @param localFilesystemRepoRoot The path to the root directory containing the module descriptors and artifacts + * @param realm The realm to use for the {@code BASIC} auth mechanism + * @param validCredentials A {@link Map} of valid credentials, the key being the user name and the value being the password, + * that the server will use during the authentication process of the incoming requests + * @return + * @throws IOException + */ + public static AutoCloseable createBasicAuthHttpServerBackedRepo(final InetSocketAddress serverAddress, final String webAppContext, + final Path localFilesystemRepoRoot, final String realm, + final Map<String, String> validCredentials) throws IOException { + final LocalFileRepoOverHttp handler = new LocalFileRepoOverHttp(webAppContext, localFilesystemRepoRoot); + final HttpServer server = HttpServer.create(serverAddress, -1); + // setup the handler + final HttpContext context = server.createContext(webAppContext, handler); + // setup basic auth on this context + final com.sun.net.httpserver.Authenticator authenticator = new BasicAuthenticator(realm) { + @Override + public boolean checkCredentials(final String user, final String pass) { + if (validCredentials == null) { + return false; + } + if (!validCredentials.containsKey(user)) { + return false; + } + final String expectedPass = validCredentials.get(user); + return expectedPass != null && expectedPass.equals(pass); + } + }; + context.setAuthenticator(authenticator); + // setup a auth filter backed by the authenticator + context.getFilters().add(new AuthFilter(authenticator)); + // start the server + server.start(); + return new AutoCloseable() { + @Override + public void close() throws Exception { + final int delaySeconds = 0; + server.stop(delaySeconds); + } + }; + } + } http://git-wip-us.apache.org/repos/asf/ant-ivy/blob/c0ccd540/test/java/org/apache/ivy/util/url/HttpclientURLHandlerTest.java ---------------------------------------------------------------------- diff --git a/test/java/org/apache/ivy/util/url/HttpclientURLHandlerTest.java b/test/java/org/apache/ivy/util/url/HttpclientURLHandlerTest.java index 0690c3e..24eb086 100644 --- a/test/java/org/apache/ivy/util/url/HttpclientURLHandlerTest.java +++ b/test/java/org/apache/ivy/util/url/HttpclientURLHandlerTest.java @@ -17,18 +17,25 @@ */ package org.apache.ivy.util.url; -import java.io.File; -import java.net.URL; - +import org.apache.ivy.TestHelper; import org.apache.ivy.core.settings.NamedTimeoutConstraint; import org.apache.ivy.core.settings.TimeoutConstraint; import org.apache.ivy.util.FileUtil; import org.apache.ivy.util.url.URLHandler.URLInfo; - import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Random; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -91,7 +98,7 @@ public class HttpclientURLHandlerTest { "http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on&zlib=on"), new File( testDir, "deflate-zlib.txt")); assertDownloadOK(new URL("http://carsten.codimi.de/gzip.yaws/daniels.html?deflate=on"), - new File(testDir, "deflate.txt")); + new File(testDir, "deflate.txt")); assertDownloadOK(new URL("http://carsten.codimi.de/gzip.yaws/a5.ps"), new File(testDir, "a5-gzip.ps")); assertDownloadOK(new URL("http://carsten.codimi.de/gzip.yaws/a5.ps?deflate=on"), new File( @@ -99,7 +106,63 @@ public class HttpclientURLHandlerTest { assertDownloadOK(new URL("http://carsten.codimi.de/gzip.yaws/nh80.pdf"), new File(testDir, "nh80-gzip.pdf")); assertDownloadOK(new URL("http://carsten.codimi.de/gzip.yaws/nh80.pdf?deflate=on"), - new File(testDir, "nh80-deflate.pdf")); + new File(testDir, "nh80-deflate.pdf")); + } + + /** + * Tests that the {@link HttpClientHandler}, backed by {@link CredentialsStore Ivy credentials store} + * works as expected when it interacts with a HTTP server which requires authentication for accessing resources. + * + * @throws Exception + * @see <a href="https://issues.apache.org/jira/browse/IVY-1336">IVY-1336</a> + */ + @Test + public void testCredentials() throws Exception { + final CredentialsStore credentialsStore = CredentialsStore.INSTANCE; + final String realm = "test-http-client-handler-realm"; + final String host = "localhost"; + final Random random = new Random(); + final String userName = "test-http-user-" + random.nextInt(); + final String password = "pass-" + random.nextInt(); + credentialsStore.addCredentials(realm, host, userName, password); + final InetSocketAddress serverBindAddr = new InetSocketAddress("localhost", 12345); + final String contextRoot = "/testHttpClientHandler"; + final Path repoRoot = new File("test/repositories").toPath(); + assertTrue(repoRoot + " is not a directory", Files.isDirectory(repoRoot)); + // create a server backed by BASIC auth with the set of "allowed" credentials + try (final AutoCloseable server = TestHelper.createBasicAuthHttpServerBackedRepo(serverBindAddr, contextRoot, + repoRoot, realm, Collections.singletonMap(userName, password))) { + + final File target = new File(testDir, "downloaded.xml"); + assertFalse("File " + target + " already exists", target.exists()); + final URL src = new URL("http://localhost:" + serverBindAddr.getPort() + "/" + + contextRoot + "/ivysettings.xml"); + // download it + handler.download(src, target, null, defaultTimeoutConstraint); + assertTrue("File " + target + " was not downloaded from " + src, target.isFile()); + } + // now create a server backed by BASIC auth with a set of credentials that do *not* match with what the + // Ivy credentials store will return for a given realm+host combination. i.e. Ivy credentials store + // will return back invalid credentials and the server will reject them + try (final AutoCloseable server = TestHelper.createBasicAuthHttpServerBackedRepo(serverBindAddr, contextRoot, + repoRoot, realm, Collections.singletonMap("other-" + userName, "other-" + password))) { + + final File target = new File(testDir, "should-not-have-been-downloaded.xml"); + assertFalse("File " + target + " already exists", target.exists()); + final URL src = new URL("http://localhost:" + serverBindAddr.getPort() + "/" + + contextRoot + "/ivysettings.xml"); + // download it (expected to fail) + try { + handler.download(src, target, null, defaultTimeoutConstraint); + Assert.fail("Download from " + src + " was expected to fail due to invalid credentials"); + } catch (IOException ioe) { + // we catch it and check for presence of 401 in the exception message. + // It's not exactly an contract that the IOException will have the 401 message but for now + // that's how it's implemented and it's fine to check for the presence of that message at the + // moment + assertTrue("Expected to find 401 error message in exception", ioe.getMessage().contains("401")); + } + } } private void assertDownloadOK(final URL url, final File file) throws Exception {