diff --git a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF index 421fa8ad6..08bd0ef14 100644 --- a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF @@ -8,6 +8,8 @@ Bundle-Localization: plugin Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Import-Package: javax.servlet;version="[2.5.0,3.2.0)", javax.servlet.http;version="[2.5.0,3.2.0)", + org.apache.commons.codec;version="1.6.0", + org.apache.commons.codec.binary;version="1.6.0", org.eclipse.jetty.continuation;version="[9.4.5,10.0.0)", org.eclipse.jetty.http;version="[9.4.5,10.0.0)", org.eclipse.jetty.io;version="[9.4.5,10.0.0)", diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java index 8cadca523..1b0c6949a 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java @@ -83,6 +83,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jgit.errors.RemoteRepositoryException; import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.errors.UnsupportedCredentialItem; import org.eclipse.jgit.http.server.GitServlet; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; @@ -105,12 +106,15 @@ import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.transport.CredentialItem; +import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchConnection; import org.eclipse.jgit.transport.HttpTransport; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.TransportHttp; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.transport.http.HttpConnectionFactory; import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory; import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory; @@ -129,12 +133,19 @@ public class SmartClientSmartServerTest extends HttpTestCase { private Repository remoteRepository; + private CredentialsProvider testCredentials = new UsernamePasswordCredentialsProvider( + AppServer.username, AppServer.password); + private URIish remoteURI; private URIish brokenURI; private URIish redirectURI; + private URIish authURI; + + private URIish authOnPostURI; + private RevBlob A_txt; private RevCommit A, B; @@ -169,7 +180,11 @@ public void setUp() throws Exception { ServletContextHandler broken = addBrokenContext(gs, src, srcName); - ServletContextHandler redirect = addRedirectContext(gs, src, srcName); + ServletContextHandler redirect = addRedirectContext(gs); + + ServletContextHandler auth = addAuthContext(gs, "auth"); + + ServletContextHandler authOnPost = addAuthContext(gs, "pauth", "POST"); server.setUp(); @@ -177,6 +192,8 @@ public void setUp() throws Exception { remoteURI = toURIish(app, srcName); brokenURI = toURIish(broken, srcName); redirectURI = toURIish(redirect, srcName); + authURI = toURIish(auth, srcName); + authOnPostURI = toURIish(authOnPost, srcName); A_txt = src.blob("A"); A = src.commit().add("A_txt", A_txt).create(); @@ -271,9 +288,14 @@ public void destroy() { return broken; } - @SuppressWarnings("unused") - private ServletContextHandler addRedirectContext(GitServlet gs, - TestRepository src, String srcName) { + private ServletContextHandler addAuthContext(GitServlet gs, + String contextPath, String... methods) { + ServletContextHandler auth = server.addContext('/' + contextPath); + auth.addServlet(new ServletHolder(gs), "/*"); + return server.authBasic(auth, methods); + } + + private ServletContextHandler addRedirectContext(GitServlet gs) { ServletContextHandler redirect = server.addContext("/redirect"); redirect.addFilter(new FilterHolder(new Filter() { @@ -283,6 +305,11 @@ private ServletContextHandler addRedirectContext(GitServlet gs, private Pattern responsePattern = Pattern .compile("/response/(\\d+)/(30[1237])/"); + // Enables tests to specify the context that the request should be + // redirected to in the end. If not present, redirects got to the + // normal /git context. + private Pattern targetPattern = Pattern.compile("/target(/\\w+)/"); + @Override public void init(FilterConfig filterConfig) throws ServletException { @@ -322,18 +349,25 @@ public void doFilter(ServletRequest request, .parseUnsignedInt(matcher.group(1)); responseCode = Integer.parseUnsignedInt(matcher.group(2)); if (--nofRedirects <= 0) { - urlString = fullUrl.substring(0, matcher.start()) + '/' - + fullUrl.substring(matcher.end()); + urlString = urlString.substring(0, matcher.start()) + + '/' + urlString.substring(matcher.end()); } else { - urlString = fullUrl.substring(0, matcher.start()) + urlString = urlString.substring(0, matcher.start()) + "/response/" + nofRedirects + "/" + responseCode + '/' - + fullUrl.substring(matcher.end()); + + urlString.substring(matcher.end()); } } httpServletResponse.setStatus(responseCode); if (nofRedirects <= 0) { - urlString = urlString.replace("/redirect", "/git"); + String targetContext = "/git"; + matcher = targetPattern.matcher(urlString); + if (matcher.find()) { + urlString = urlString.substring(0, matcher.start()) + + '/' + urlString.substring(matcher.end()); + targetContext = matcher.group(1); + } + urlString = urlString.replace("/redirect", targetContext); } httpServletResponse.setHeader(HttpSupport.HDR_LOCATION, urlString); @@ -668,6 +702,215 @@ public void testInitialClone_RedirectForbidden() throws Exception { } } + @Test + public void testInitialClone_WithAuthentication() throws Exception { + Repository dst = createBareRepository(); + assertFalse(dst.hasObject(A_txt)); + + try (Transport t = Transport.open(dst, authURI)) { + t.setCredentialsProvider(testCredentials); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + } + + assertTrue(dst.hasObject(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + + List requests = getRequests(); + assertEquals(3, requests.size()); + + AccessEvent info = requests.get(0); + assertEquals("GET", info.getMethod()); + assertEquals(401, info.getStatus()); + + info = requests.get(1); + assertEquals("GET", info.getMethod()); + assertEquals(join(authURI, "info/refs"), info.getPath()); + assertEquals(1, info.getParameters().size()); + assertEquals("git-upload-pack", info.getParameter("service")); + assertEquals(200, info.getStatus()); + assertEquals("application/x-git-upload-pack-advertisement", + info.getResponseHeader(HDR_CONTENT_TYPE)); + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + + AccessEvent service = requests.get(2); + assertEquals("POST", service.getMethod()); + assertEquals(join(authURI, "git-upload-pack"), service.getPath()); + assertEquals(0, service.getParameters().size()); + assertNotNull("has content-length", + service.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + service.getRequestHeader(HDR_TRANSFER_ENCODING)); + + assertEquals(200, service.getStatus()); + assertEquals("application/x-git-upload-pack-result", + service.getResponseHeader(HDR_CONTENT_TYPE)); + } + + @Test + public void testInitialClone_WithAuthenticationNoCredentials() + throws Exception { + Repository dst = createBareRepository(); + assertFalse(dst.hasObject(A_txt)); + + try (Transport t = Transport.open(dst, authURI)) { + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + fail("Should not have succeeded -- no authentication"); + } catch (TransportException e) { + String msg = e.getMessage(); + assertTrue("Unexpected exception message: " + msg, + msg.contains("no CredentialsProvider")); + } + List requests = getRequests(); + assertEquals(1, requests.size()); + + AccessEvent info = requests.get(0); + assertEquals("GET", info.getMethod()); + assertEquals(401, info.getStatus()); + } + + @Test + public void testInitialClone_WithAuthenticationWrongCredentials() + throws Exception { + Repository dst = createBareRepository(); + assertFalse(dst.hasObject(A_txt)); + + try (Transport t = Transport.open(dst, authURI)) { + t.setCredentialsProvider(new UsernamePasswordCredentialsProvider( + AppServer.username, "wrongpassword")); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + fail("Should not have succeeded -- wrong password"); + } catch (TransportException e) { + String msg = e.getMessage(); + assertTrue("Unexpected exception message: " + msg, + msg.contains("auth")); + } + List requests = getRequests(); + // Once without authentication plus three re-tries with authentication + assertEquals(4, requests.size()); + + for (AccessEvent event : requests) { + assertEquals("GET", event.getMethod()); + assertEquals(401, event.getStatus()); + } + } + + @Test + public void testInitialClone_WithAuthenticationAfterRedirect() + throws Exception { + Repository dst = createBareRepository(); + assertFalse(dst.hasObject(A_txt)); + + URIish cloneFrom = extendPath(redirectURI, "/target/auth"); + CredentialsProvider uriSpecificCredentialsProvider = new UsernamePasswordCredentialsProvider( + "unknown", "none") { + @Override + public boolean get(URIish uri, CredentialItem... items) + throws UnsupportedCredentialItem { + // Only return the true credentials if the uri path starts with + // /auth. This ensures that we do provide the correct + // credentials only for the URi after the redirect, making the + // test fail if we should be asked for the credentials for the + // original URI. + if (uri.getPath().startsWith("/auth")) { + return testCredentials.get(uri, items); + } + return super.get(uri, items); + } + }; + try (Transport t = Transport.open(dst, cloneFrom)) { + t.setCredentialsProvider(uriSpecificCredentialsProvider); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + } + + assertTrue(dst.hasObject(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + + List requests = getRequests(); + assertEquals(4, requests.size()); + + AccessEvent redirect = requests.get(0); + assertEquals("GET", redirect.getMethod()); + assertEquals(join(cloneFrom, "info/refs"), redirect.getPath()); + assertEquals(301, redirect.getStatus()); + + AccessEvent info = requests.get(1); + assertEquals("GET", info.getMethod()); + assertEquals(join(authURI, "info/refs"), info.getPath()); + assertEquals(401, info.getStatus()); + + info = requests.get(2); + assertEquals("GET", info.getMethod()); + assertEquals(join(authURI, "info/refs"), info.getPath()); + assertEquals(1, info.getParameters().size()); + assertEquals("git-upload-pack", info.getParameter("service")); + assertEquals(200, info.getStatus()); + assertEquals("application/x-git-upload-pack-advertisement", + info.getResponseHeader(HDR_CONTENT_TYPE)); + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + + AccessEvent service = requests.get(3); + assertEquals("POST", service.getMethod()); + assertEquals(join(authURI, "git-upload-pack"), service.getPath()); + assertEquals(0, service.getParameters().size()); + assertNotNull("has content-length", + service.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + service.getRequestHeader(HDR_TRANSFER_ENCODING)); + + assertEquals(200, service.getStatus()); + assertEquals("application/x-git-upload-pack-result", + service.getResponseHeader(HDR_CONTENT_TYPE)); + } + + @Test + public void testInitialClone_WithAuthenticationOnPostOnly() + throws Exception { + Repository dst = createBareRepository(); + assertFalse(dst.hasObject(A_txt)); + + try (Transport t = Transport.open(dst, authOnPostURI)) { + t.setCredentialsProvider(testCredentials); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + } + + assertTrue(dst.hasObject(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + + List requests = getRequests(); + assertEquals(3, requests.size()); + + AccessEvent info = requests.get(0); + assertEquals("GET", info.getMethod()); + assertEquals(join(authOnPostURI, "info/refs"), info.getPath()); + assertEquals(1, info.getParameters().size()); + assertEquals("git-upload-pack", info.getParameter("service")); + assertEquals(200, info.getStatus()); + assertEquals("application/x-git-upload-pack-advertisement", + info.getResponseHeader(HDR_CONTENT_TYPE)); + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + + AccessEvent service = requests.get(1); + assertEquals("POST", service.getMethod()); + assertEquals(join(authOnPostURI, "git-upload-pack"), service.getPath()); + assertEquals(401, service.getStatus()); + + service = requests.get(2); + assertEquals("POST", service.getMethod()); + assertEquals(join(authOnPostURI, "git-upload-pack"), service.getPath()); + assertEquals(0, service.getParameters().size()); + assertNotNull("has content-length", + service.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + service.getRequestHeader(HDR_TRANSFER_ENCODING)); + + assertEquals(200, service.getStatus()); + assertEquals("application/x-git-upload-pack-result", + service.getResponseHeader(HDR_CONTENT_TYPE)); + } + @Test public void testFetch_FewLocalCommits() throws Exception { // Bootstrap by doing the clone. diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java index 69e2cd595..e257cf65b 100644 --- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java +++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java @@ -55,6 +55,7 @@ import java.nio.file.Files; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -97,6 +98,9 @@ public class AppServer { /** SSL keystore password; must have at least 6 characters. */ private static final String keyPassword = "mykeys"; + /** Role for authentication. */ + private static final String authRole = "can-access"; + static { // Install a logger that throws warning messages. // @@ -136,10 +140,10 @@ public AppServer(int port) { /** * @param port - * for https, may be zero to allocate a port dynamically + * for http, may be zero to allocate a port dynamically * @param sslPort * for https,may be zero to allocate a port dynamically. If - * negative, the server will be set up without https support.. + * negative, the server will be set up without https support. * @since 4.9 */ public AppServer(int port, int sslPort) { @@ -264,9 +268,10 @@ public ServletContextHandler addContext(String path) { return ctx; } - public ServletContextHandler authBasic(ServletContextHandler ctx) { + public ServletContextHandler authBasic(ServletContextHandler ctx, + String... methods) { assertNotYetSetUp(); - auth(ctx, new BasicAuthenticator()); + auth(ctx, new BasicAuthenticator(), methods); return ctx; } @@ -301,22 +306,36 @@ protected UserPrincipal loadUserInfo(String user) { } } - private void auth(ServletContextHandler ctx, Authenticator authType) { - final String role = "can-access"; - - AbstractLoginService users = new TestMappedLoginService(role); + private ConstraintMapping createConstraintMapping() { ConstraintMapping cm = new ConstraintMapping(); cm.setConstraint(new Constraint()); cm.getConstraint().setAuthenticate(true); cm.getConstraint().setDataConstraint(Constraint.DC_NONE); - cm.getConstraint().setRoles(new String[] { role }); + cm.getConstraint().setRoles(new String[] { authRole }); cm.setPathSpec("/*"); + return cm; + } + + private void auth(ServletContextHandler ctx, Authenticator authType, + String... methods) { + AbstractLoginService users = new TestMappedLoginService(authRole); + List mappings = new ArrayList<>(); + if (methods == null || methods.length == 0) { + mappings.add(createConstraintMapping()); + } else { + for (String method : methods) { + ConstraintMapping cm = createConstraintMapping(); + cm.setMethod(method.toUpperCase(Locale.ROOT)); + mappings.add(cm); + } + } ConstraintSecurityHandler sec = new ConstraintSecurityHandler(); sec.setRealmName(realm); sec.setAuthenticator(authType); sec.setLoginService(users); - sec.setConstraintMappings(new ConstraintMapping[] { cm }); + sec.setConstraintMappings( + mappings.toArray(new ConstraintMapping[mappings.size()])); sec.setHandler(ctx); contexts.removeHandler(ctx); diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index bf2a4a2e0..c532b328d 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -365,7 +365,7 @@ invalidPathContainsSeparator=Invalid path (contains separator ''{0}''): {1} invalidPathPeriodAtEndWindows=Invalid path (period at end is ignored by Windows): {0} invalidPathSpaceAtEndWindows=Invalid path (space at end is ignored by Windows): {0} invalidPathReservedOnWindows=Invalid path (''{0}'' is reserved on Windows): {1} -invalidRedirectLocation=Redirect or URI ''{0}'': invalid redirect location {1} -> {2} +invalidRedirectLocation=Invalid redirect location {1} -> {2} invalidReflogRevision=Invalid reflog revision: {0} invalidRefName=Invalid ref name: {0} invalidReftableBlock=Invalid reftable block @@ -587,7 +587,7 @@ secondsAgo={0} seconds ago selectingCommits=Selecting commits sequenceTooLargeForDiffAlgorithm=Sequence too large for difference algorithm. serviceNotEnabledNoName=Service not enabled -serviceNotPermitted={0} not permitted +serviceNotPermitted={1} not permitted on ''{0}'' sha1CollisionDetected1=SHA-1 collision detected on {0} shallowCommitsAlreadyInitialized=Shallow commits have already been initialized shallowPacksRequireDepthWalk=Shallow packs require a DepthWalk @@ -636,6 +636,7 @@ timeIsUncertain=Time is uncertain timerAlreadyTerminated=Timer already terminated tooManyCommands=Too many commands tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)? +tooManyRedirects=Too many redirects; stopped after {0} redirects at ''{1}'' topologicalSortRequired=Topological sort required. transactionAborted=transaction aborted transportExceptionBadRef=Empty ref: {0}: {1} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 07666eb93..5acf05876 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -696,6 +696,7 @@ public static JGitText get() { /***/ public String timerAlreadyTerminated; /***/ public String tooManyCommands; /***/ public String tooManyIncludeRecursions; + /***/ public String tooManyRedirects; /***/ public String topologicalSortRequired; /***/ public String transportExceptionBadRef; /***/ public String transportExceptionEmptyRef; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 327dc39c7..1647200e6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -323,6 +323,13 @@ private static class HttpConfig { } } + /** + * The current URI we're talking to. The inherited (final) field + * {@link #uri} stores the original URI; {@code currentUri} may be different + * after redirects. + */ + private URIish currentUri; + private URL baseUrl; private URL objectsUrl; @@ -360,6 +367,7 @@ private URL toURL(URIish urish) throws MalformedURLException { */ protected void setURI(final URIish uri) throws NotSupportedException { try { + currentUri = uri; baseUrl = toURL(uri); objectsUrl = new URL(baseUrl, "objects/"); //$NON-NLS-1$ } catch (MalformedURLException e) { @@ -584,9 +592,10 @@ private HttpConnection connect(final String service) throw new TransportException(uri, JGitText.get().noCredentialsProvider); if (authAttempts > 1) - credentialsProvider.reset(uri); + credentialsProvider.reset(currentUri); if (3 < authAttempts - || !authMethod.authorize(uri, credentialsProvider)) { + || !authMethod.authorize(currentUri, + credentialsProvider)) { throw new TransportException(uri, JGitText.get().notAuthorized); } @@ -1096,8 +1105,17 @@ void sendRequest() throws IOException { buf = out; } + HttpAuthMethod authenticator = null; + Collection ignoreTypes = EnumSet.noneOf(Type.class); + // Counts number of repeated authentication attempts using the same + // authentication scheme + int authAttempts = 1; int redirects = 0; for (;;) { + // The very first time we will try with the authentication + // method used on the initial GET request. This is a hint only; + // it may fail. If so, we'll then re-try with proper 401 + // handling, going through the available authentication schemes. openStream(); if (buf != out) { conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP); @@ -1107,31 +1125,111 @@ void sendRequest() throws IOException { buf.writeTo(httpOut, null); } - if (http.followRedirects == HttpRedirectMode.TRUE) { - final int status = HttpSupport.response(conn); - switch (status) { - case HttpConnection.HTTP_MOVED_PERM: - case HttpConnection.HTTP_MOVED_TEMP: - case HttpConnection.HTTP_11_MOVED_TEMP: - // SEE_OTHER after a POST doesn't make sense for a git - // server, so we don't handle it here and thus we'll - // report an error in openResponse() later on. - URIish newUri = redirect( - conn.getHeaderField(HDR_LOCATION), - '/' + serviceName, redirects++); - try { - baseUrl = toURL(newUri); - } catch (MalformedURLException e) { - throw new TransportException(MessageFormat.format( - JGitText.get().invalidRedirectLocation, - uri, baseUrl, newUri), e); + final int status = HttpSupport.response(conn); + switch (status) { + case HttpConnection.HTTP_OK: + // We're done. + return; + + case HttpConnection.HTTP_NOT_FOUND: + throw new NoRemoteRepositoryException(uri, MessageFormat + .format(JGitText.get().uriNotFound, conn.getURL())); + + case HttpConnection.HTTP_FORBIDDEN: + throw new TransportException(uri, + MessageFormat.format( + JGitText.get().serviceNotPermitted, + baseUrl, serviceName)); + + case HttpConnection.HTTP_MOVED_PERM: + case HttpConnection.HTTP_MOVED_TEMP: + case HttpConnection.HTTP_11_MOVED_TEMP: + // SEE_OTHER after a POST doesn't make sense for a git + // server, so we don't handle it here and thus we'll + // report an error in openResponse() later on. + if (http.followRedirects != HttpRedirectMode.TRUE) { + // Let openResponse() issue an error + return; + } + currentUri = redirect( + conn.getHeaderField(HDR_LOCATION), + '/' + serviceName, redirects++); + try { + baseUrl = toURL(currentUri); + } catch (MalformedURLException e) { + throw new TransportException(uri, MessageFormat.format( + JGitText.get().invalidRedirectLocation, + baseUrl, currentUri), e); + } + continue; + + case HttpConnection.HTTP_UNAUTHORIZED: + HttpAuthMethod nextMethod = HttpAuthMethod + .scanResponse(conn, ignoreTypes); + switch (nextMethod.getType()) { + case NONE: + throw new TransportException(uri, + MessageFormat.format( + JGitText.get().authenticationNotSupported, + conn.getURL())); + case NEGOTIATE: + // RFC 4559 states "When using the SPNEGO [...] with + // [...] POST, the authentication should be complete + // [...] before sending the user data." So in theory + // the initial GET should have been authenticated + // already. (Unless there was a redirect?) + // + // We try this only once: + ignoreTypes.add(HttpAuthMethod.Type.NEGOTIATE); + if (authenticator != null) { + ignoreTypes.add(authenticator.getType()); } - continue; + authAttempts = 1; + // We only do the Kerberos part of SPNEGO, which + // requires only one attempt. We do *not* to the + // NTLM part of SPNEGO; it's a multi-round + // negotiation and among other problems it would + // be unclear when to stop if no HTTP_OK is + // forthcoming. In theory a malicious server + // could keep sending requests for another NTLM + // round, keeping a client stuck here. + break; default: + // DIGEST or BASIC. Let's be sure we ignore NEGOTIATE; + // if it was available, we have tried it before. + ignoreTypes.add(HttpAuthMethod.Type.NEGOTIATE); + if (authenticator == null || authenticator + .getType() != nextMethod.getType()) { + if (authenticator != null) { + ignoreTypes.add(authenticator.getType()); + } + authAttempts = 1; + } break; } + authMethod = nextMethod; + authenticator = nextMethod; + CredentialsProvider credentialsProvider = getCredentialsProvider(); + if (credentialsProvider == null) { + throw new TransportException(uri, + JGitText.get().noCredentialsProvider); + } + if (authAttempts > 1) { + credentialsProvider.reset(currentUri); + } + if (3 < authAttempts || !authMethod.authorize(currentUri, + credentialsProvider)) { + throw new TransportException(uri, + JGitText.get().notAuthorized); + } + authAttempts++; + continue; + + default: + // Just return here; openResponse() will report an appropriate + // error. + return; } - break; } }