Do authentication re-tries on HTTP POST

There is at least one git server out there (GOGS) that does
not require authentication on the initial GET for
info/refs?service=git-receive-pack but that _does_ require
authentication for the subsequent POST to actually do the push.

This occurs on GOGS with public repositories; for private
repositories it wants authentication up front.

Handle this behavior by adding 401 handling to our POST request.
Note that this is suboptimal; we'll re-send the push data at
least twice if an authentication failure on POST occurs. It
would be much better if the server required authentication
up-front in the GET request.

Added authentication unit tests (using BASIC auth) to the
SmartClientSmartServerTest:

- clone with authentication
- clone with authentication but lacking CredentialsProvider
- clone with authentication and wrong password
- clone with authentication after redirect
- clone with authentication only on POST, but not on GET

Also tested manually in the wild using repositories at try.gogs.io.
That server offers only BASIC auth, so the other paths
(DIGEST, NEGOTIATE, fall back from DIGEST to BASIC) are untested
and I have no way to test them.

* public repository: GET unauthenticated, POST authenticated
  Also tested after clearing the credentials and then entering a
  wrong password: correctly asks three times during the HTTP
  POST for user name and password, then gives up.
* private repository: authentication already on GET; then gets
  applied correctly initially to the POST request, which succeeds.

Also fix the authentication to use the credentials for the redirected
URI if redirects had occurred. We must not present the credentials
for the original URI in that case. Consider a malicious redirect A->B:
this would allow server B to harvest the user credentials for server
A. The unit test for authentication after a redirect also tests for
this.

Bug: 513043
Change-Id: I97ee5058569efa1545a6c6f6edfd2b357c40592a
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
Thomas Wolf 2017-06-16 10:25:53 +02:00
parent 231f5d9baf
commit 7ac1bfc834
6 changed files with 407 additions and 43 deletions

View File

@ -8,6 +8,8 @@ Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: javax.servlet;version="[2.5.0,3.2.0)", Import-Package: javax.servlet;version="[2.5.0,3.2.0)",
javax.servlet.http;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.continuation;version="[9.4.5,10.0.0)",
org.eclipse.jetty.http;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)", org.eclipse.jetty.io;version="[9.4.5,10.0.0)",

View File

@ -83,6 +83,7 @@
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jgit.errors.RemoteRepositoryException; import org.eclipse.jgit.errors.RemoteRepositoryException;
import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.http.server.GitServlet; import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
@ -105,12 +106,15 @@
import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.FileBasedConfig; 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.FetchConnection;
import org.eclipse.jgit.transport.HttpTransport; import org.eclipse.jgit.transport.HttpTransport;
import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.TransportHttp; import org.eclipse.jgit.transport.TransportHttp;
import org.eclipse.jgit.transport.URIish; 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.HttpConnectionFactory;
import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory; import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory;
import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory; import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
@ -129,12 +133,19 @@ public class SmartClientSmartServerTest extends HttpTestCase {
private Repository remoteRepository; private Repository remoteRepository;
private CredentialsProvider testCredentials = new UsernamePasswordCredentialsProvider(
AppServer.username, AppServer.password);
private URIish remoteURI; private URIish remoteURI;
private URIish brokenURI; private URIish brokenURI;
private URIish redirectURI; private URIish redirectURI;
private URIish authURI;
private URIish authOnPostURI;
private RevBlob A_txt; private RevBlob A_txt;
private RevCommit A, B; private RevCommit A, B;
@ -169,7 +180,11 @@ public void setUp() throws Exception {
ServletContextHandler broken = addBrokenContext(gs, src, srcName); 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(); server.setUp();
@ -177,6 +192,8 @@ public void setUp() throws Exception {
remoteURI = toURIish(app, srcName); remoteURI = toURIish(app, srcName);
brokenURI = toURIish(broken, srcName); brokenURI = toURIish(broken, srcName);
redirectURI = toURIish(redirect, srcName); redirectURI = toURIish(redirect, srcName);
authURI = toURIish(auth, srcName);
authOnPostURI = toURIish(authOnPost, srcName);
A_txt = src.blob("A"); A_txt = src.blob("A");
A = src.commit().add("A_txt", A_txt).create(); A = src.commit().add("A_txt", A_txt).create();
@ -271,9 +288,14 @@ public void destroy() {
return broken; return broken;
} }
@SuppressWarnings("unused") private ServletContextHandler addAuthContext(GitServlet gs,
private ServletContextHandler addRedirectContext(GitServlet gs, String contextPath, String... methods) {
TestRepository<Repository> src, String srcName) { 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"); ServletContextHandler redirect = server.addContext("/redirect");
redirect.addFilter(new FilterHolder(new Filter() { redirect.addFilter(new FilterHolder(new Filter() {
@ -283,6 +305,11 @@ private ServletContextHandler addRedirectContext(GitServlet gs,
private Pattern responsePattern = Pattern private Pattern responsePattern = Pattern
.compile("/response/(\\d+)/(30[1237])/"); .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 @Override
public void init(FilterConfig filterConfig) public void init(FilterConfig filterConfig)
throws ServletException { throws ServletException {
@ -322,18 +349,25 @@ public void doFilter(ServletRequest request,
.parseUnsignedInt(matcher.group(1)); .parseUnsignedInt(matcher.group(1));
responseCode = Integer.parseUnsignedInt(matcher.group(2)); responseCode = Integer.parseUnsignedInt(matcher.group(2));
if (--nofRedirects <= 0) { if (--nofRedirects <= 0) {
urlString = fullUrl.substring(0, matcher.start()) + '/' urlString = urlString.substring(0, matcher.start())
+ fullUrl.substring(matcher.end()); + '/' + urlString.substring(matcher.end());
} else { } else {
urlString = fullUrl.substring(0, matcher.start()) urlString = urlString.substring(0, matcher.start())
+ "/response/" + nofRedirects + "/" + "/response/" + nofRedirects + "/"
+ responseCode + '/' + responseCode + '/'
+ fullUrl.substring(matcher.end()); + urlString.substring(matcher.end());
} }
} }
httpServletResponse.setStatus(responseCode); httpServletResponse.setStatus(responseCode);
if (nofRedirects <= 0) { 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, httpServletResponse.setHeader(HttpSupport.HDR_LOCATION,
urlString); 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<AccessEvent> 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<AccessEvent> 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<AccessEvent> 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<AccessEvent> 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<AccessEvent> 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 @Test
public void testFetch_FewLocalCommits() throws Exception { public void testFetch_FewLocalCommits() throws Exception {
// Bootstrap by doing the clone. // Bootstrap by doing the clone.

View File

@ -55,6 +55,7 @@
import java.nio.file.Files; import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@ -97,6 +98,9 @@ public class AppServer {
/** SSL keystore password; must have at least 6 characters. */ /** SSL keystore password; must have at least 6 characters. */
private static final String keyPassword = "mykeys"; private static final String keyPassword = "mykeys";
/** Role for authentication. */
private static final String authRole = "can-access";
static { static {
// Install a logger that throws warning messages. // Install a logger that throws warning messages.
// //
@ -136,10 +140,10 @@ public AppServer(int port) {
/** /**
* @param 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 * @param sslPort
* for https,may be zero to allocate a port dynamically. If * 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 * @since 4.9
*/ */
public AppServer(int port, int sslPort) { public AppServer(int port, int sslPort) {
@ -264,9 +268,10 @@ public ServletContextHandler addContext(String path) {
return ctx; return ctx;
} }
public ServletContextHandler authBasic(ServletContextHandler ctx) { public ServletContextHandler authBasic(ServletContextHandler ctx,
String... methods) {
assertNotYetSetUp(); assertNotYetSetUp();
auth(ctx, new BasicAuthenticator()); auth(ctx, new BasicAuthenticator(), methods);
return ctx; return ctx;
} }
@ -301,22 +306,36 @@ protected UserPrincipal loadUserInfo(String user) {
} }
} }
private void auth(ServletContextHandler ctx, Authenticator authType) { private ConstraintMapping createConstraintMapping() {
final String role = "can-access";
AbstractLoginService users = new TestMappedLoginService(role);
ConstraintMapping cm = new ConstraintMapping(); ConstraintMapping cm = new ConstraintMapping();
cm.setConstraint(new Constraint()); cm.setConstraint(new Constraint());
cm.getConstraint().setAuthenticate(true); cm.getConstraint().setAuthenticate(true);
cm.getConstraint().setDataConstraint(Constraint.DC_NONE); cm.getConstraint().setDataConstraint(Constraint.DC_NONE);
cm.getConstraint().setRoles(new String[] { role }); cm.getConstraint().setRoles(new String[] { authRole });
cm.setPathSpec("/*"); cm.setPathSpec("/*");
return cm;
}
private void auth(ServletContextHandler ctx, Authenticator authType,
String... methods) {
AbstractLoginService users = new TestMappedLoginService(authRole);
List<ConstraintMapping> 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(); ConstraintSecurityHandler sec = new ConstraintSecurityHandler();
sec.setRealmName(realm); sec.setRealmName(realm);
sec.setAuthenticator(authType); sec.setAuthenticator(authType);
sec.setLoginService(users); sec.setLoginService(users);
sec.setConstraintMappings(new ConstraintMapping[] { cm }); sec.setConstraintMappings(
mappings.toArray(new ConstraintMapping[mappings.size()]));
sec.setHandler(ctx); sec.setHandler(ctx);
contexts.removeHandler(ctx); contexts.removeHandler(ctx);

View File

@ -365,7 +365,7 @@ invalidPathContainsSeparator=Invalid path (contains separator ''{0}''): {1}
invalidPathPeriodAtEndWindows=Invalid path (period at end is ignored by Windows): {0} invalidPathPeriodAtEndWindows=Invalid path (period at end is ignored by Windows): {0}
invalidPathSpaceAtEndWindows=Invalid path (space 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} 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} invalidReflogRevision=Invalid reflog revision: {0}
invalidRefName=Invalid ref name: {0} invalidRefName=Invalid ref name: {0}
invalidReftableBlock=Invalid reftable block invalidReftableBlock=Invalid reftable block
@ -587,7 +587,7 @@ secondsAgo={0} seconds ago
selectingCommits=Selecting commits selectingCommits=Selecting commits
sequenceTooLargeForDiffAlgorithm=Sequence too large for difference algorithm. sequenceTooLargeForDiffAlgorithm=Sequence too large for difference algorithm.
serviceNotEnabledNoName=Service not enabled serviceNotEnabledNoName=Service not enabled
serviceNotPermitted={0} not permitted serviceNotPermitted={1} not permitted on ''{0}''
sha1CollisionDetected1=SHA-1 collision detected on {0} sha1CollisionDetected1=SHA-1 collision detected on {0}
shallowCommitsAlreadyInitialized=Shallow commits have already been initialized shallowCommitsAlreadyInitialized=Shallow commits have already been initialized
shallowPacksRequireDepthWalk=Shallow packs require a DepthWalk shallowPacksRequireDepthWalk=Shallow packs require a DepthWalk
@ -636,6 +636,7 @@ timeIsUncertain=Time is uncertain
timerAlreadyTerminated=Timer already terminated timerAlreadyTerminated=Timer already terminated
tooManyCommands=Too many commands tooManyCommands=Too many commands
tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)? tooManyIncludeRecursions=Too many recursions; circular includes in config file(s)?
tooManyRedirects=Too many redirects; stopped after {0} redirects at ''{1}''
topologicalSortRequired=Topological sort required. topologicalSortRequired=Topological sort required.
transactionAborted=transaction aborted transactionAborted=transaction aborted
transportExceptionBadRef=Empty ref: {0}: {1} transportExceptionBadRef=Empty ref: {0}: {1}

View File

@ -696,6 +696,7 @@ public static JGitText get() {
/***/ public String timerAlreadyTerminated; /***/ public String timerAlreadyTerminated;
/***/ public String tooManyCommands; /***/ public String tooManyCommands;
/***/ public String tooManyIncludeRecursions; /***/ public String tooManyIncludeRecursions;
/***/ public String tooManyRedirects;
/***/ public String topologicalSortRequired; /***/ public String topologicalSortRequired;
/***/ public String transportExceptionBadRef; /***/ public String transportExceptionBadRef;
/***/ public String transportExceptionEmptyRef; /***/ public String transportExceptionEmptyRef;

View File

@ -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 baseUrl;
private URL objectsUrl; private URL objectsUrl;
@ -360,6 +367,7 @@ private URL toURL(URIish urish) throws MalformedURLException {
*/ */
protected void setURI(final URIish uri) throws NotSupportedException { protected void setURI(final URIish uri) throws NotSupportedException {
try { try {
currentUri = uri;
baseUrl = toURL(uri); baseUrl = toURL(uri);
objectsUrl = new URL(baseUrl, "objects/"); //$NON-NLS-1$ objectsUrl = new URL(baseUrl, "objects/"); //$NON-NLS-1$
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
@ -584,9 +592,10 @@ private HttpConnection connect(final String service)
throw new TransportException(uri, throw new TransportException(uri,
JGitText.get().noCredentialsProvider); JGitText.get().noCredentialsProvider);
if (authAttempts > 1) if (authAttempts > 1)
credentialsProvider.reset(uri); credentialsProvider.reset(currentUri);
if (3 < authAttempts if (3 < authAttempts
|| !authMethod.authorize(uri, credentialsProvider)) { || !authMethod.authorize(currentUri,
credentialsProvider)) {
throw new TransportException(uri, throw new TransportException(uri,
JGitText.get().notAuthorized); JGitText.get().notAuthorized);
} }
@ -1096,8 +1105,17 @@ void sendRequest() throws IOException {
buf = out; buf = out;
} }
HttpAuthMethod authenticator = null;
Collection<Type> ignoreTypes = EnumSet.noneOf(Type.class);
// Counts number of repeated authentication attempts using the same
// authentication scheme
int authAttempts = 1;
int redirects = 0; int redirects = 0;
for (;;) { 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(); openStream();
if (buf != out) { if (buf != out) {
conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP); conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP);
@ -1107,31 +1125,111 @@ void sendRequest() throws IOException {
buf.writeTo(httpOut, null); buf.writeTo(httpOut, null);
} }
if (http.followRedirects == HttpRedirectMode.TRUE) { final int status = HttpSupport.response(conn);
final int status = HttpSupport.response(conn); switch (status) {
switch (status) { case HttpConnection.HTTP_OK:
case HttpConnection.HTTP_MOVED_PERM: // We're done.
case HttpConnection.HTTP_MOVED_TEMP: return;
case HttpConnection.HTTP_11_MOVED_TEMP:
// SEE_OTHER after a POST doesn't make sense for a git case HttpConnection.HTTP_NOT_FOUND:
// server, so we don't handle it here and thus we'll throw new NoRemoteRepositoryException(uri, MessageFormat
// report an error in openResponse() later on. .format(JGitText.get().uriNotFound, conn.getURL()));
URIish newUri = redirect(
conn.getHeaderField(HDR_LOCATION), case HttpConnection.HTTP_FORBIDDEN:
'/' + serviceName, redirects++); throw new TransportException(uri,
try { MessageFormat.format(
baseUrl = toURL(newUri); JGitText.get().serviceNotPermitted,
} catch (MalformedURLException e) { baseUrl, serviceName));
throw new TransportException(MessageFormat.format(
JGitText.get().invalidRedirectLocation, case HttpConnection.HTTP_MOVED_PERM:
uri, baseUrl, newUri), e); 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: 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; 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;
} }
} }