diff options
4 files changed, 254 insertions, 21 deletions
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 89a254184d..887e970a0c 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 @@ -936,26 +936,8 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { } } - @Test - public void testInitialClone_WithAuthentication() throws Exception { - try (Repository dst = createBareRepository(); - Transport t = Transport.open(dst, authURI)) { - assertFalse(dst.getObjectDatabase().has(A_txt)); - t.setCredentialsProvider(testCredentials); - t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - assertTrue(dst.getObjectDatabase().has(A_txt)); - assertEquals(B, dst.exactRef(master).getObjectId()); - fsck(dst, B); - } - - List<AccessEvent> requests = getRequests(); - assertEquals(enableProtocolV2 ? 4 : 3, requests.size()); - - AccessEvent info = requests.get(0); - assertEquals("GET", info.getMethod()); - assertEquals(401, info.getStatus()); - - info = requests.get(1); + private void assertFetchRequests(List<AccessEvent> requests, int index) { + AccessEvent info = requests.get(index++); assertEquals("GET", info.getMethod()); assertEquals(join(authURI, "info/refs"), info.getPath()); assertEquals(1, info.getParameters().size()); @@ -967,7 +949,7 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); } - for (int i = 2; i < requests.size(); i++) { + for (int i = index; i < requests.size(); i++) { AccessEvent service = requests.get(i); assertEquals("POST", service.getMethod()); assertEquals(join(authURI, "git-upload-pack"), service.getPath()); @@ -984,6 +966,182 @@ public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { } @Test + public void testInitialClone_WithAuthentication() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.setCredentialsProvider(testCredentials); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + assertTrue(dst.getObjectDatabase().has(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + } + + List<AccessEvent> requests = getRequests(); + assertEquals(enableProtocolV2 ? 4 : 3, requests.size()); + + AccessEvent info = requests.get(0); + assertEquals("GET", info.getMethod()); + assertEquals(401, info.getStatus()); + + assertFetchRequests(requests, 1); + } + + @Test + public void testInitialClone_WithPreAuthentication() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + ((TransportHttp) t).setPreemptiveBasicAuthentication( + AppServer.username, AppServer.password); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + assertTrue(dst.getObjectDatabase().has(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + } + + List<AccessEvent> requests = getRequests(); + assertEquals(enableProtocolV2 ? 3 : 2, requests.size()); + + assertFetchRequests(requests, 0); + } + + @Test + public void testInitialClone_WithPreAuthenticationCleared() + throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + ((TransportHttp) t).setPreemptiveBasicAuthentication( + AppServer.username, AppServer.password); + ((TransportHttp) t).setPreemptiveBasicAuthentication(null, null); + t.setCredentialsProvider(testCredentials); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + assertTrue(dst.getObjectDatabase().has(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + } + + List<AccessEvent> requests = getRequests(); + assertEquals(enableProtocolV2 ? 4 : 3, requests.size()); + + AccessEvent info = requests.get(0); + assertEquals("GET", info.getMethod()); + assertEquals(401, info.getStatus()); + + assertFetchRequests(requests, 1); + } + + @Test + public void testInitialClone_PreAuthenticationTooLate() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + ((TransportHttp) t).setPreemptiveBasicAuthentication( + AppServer.username, AppServer.password); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + assertTrue(dst.getObjectDatabase().has(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + List<AccessEvent> requests = getRequests(); + assertEquals(enableProtocolV2 ? 3 : 2, requests.size()); + assertFetchRequests(requests, 0); + assertThrows(IllegalStateException.class, + () -> ((TransportHttp) t).setPreemptiveBasicAuthentication( + AppServer.username, AppServer.password)); + assertThrows(IllegalStateException.class, () -> ((TransportHttp) t) + .setPreemptiveBasicAuthentication(null, null)); + } + } + + @Test + public void testInitialClone_WithWrongPreAuthenticationAndCredentialProvider() + throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + ((TransportHttp) t).setPreemptiveBasicAuthentication( + AppServer.username, AppServer.password + 'x'); + t.setCredentialsProvider(testCredentials); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + assertTrue(dst.getObjectDatabase().has(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + } + + List<AccessEvent> requests = getRequests(); + assertEquals(enableProtocolV2 ? 4 : 3, requests.size()); + + AccessEvent info = requests.get(0); + assertEquals("GET", info.getMethod()); + assertEquals(401, info.getStatus()); + + assertFetchRequests(requests, 1); + } + + @Test + public void testInitialClone_WithWrongPreAuthentication() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + ((TransportHttp) t).setPreemptiveBasicAuthentication( + AppServer.username, AppServer.password + 'x'); + TransportException e = assertThrows(TransportException.class, + () -> t.fetch(NullProgressMonitor.INSTANCE, + mirror(master))); + 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_WithUserInfo() throws Exception { + URIish withUserInfo = authURI.setUser(AppServer.username) + .setPass(AppServer.password); + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, withUserInfo)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + assertTrue(dst.getObjectDatabase().has(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + } + + List<AccessEvent> requests = getRequests(); + assertEquals(enableProtocolV2 ? 3 : 2, requests.size()); + + assertFetchRequests(requests, 0); + } + + @Test + public void testInitialClone_PreAuthOverridesUserInfo() throws Exception { + URIish withUserInfo = authURI.setUser(AppServer.username) + .setPass(AppServer.password + 'x'); + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, withUserInfo)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + ((TransportHttp) t).setPreemptiveBasicAuthentication( + AppServer.username, AppServer.password); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + assertTrue(dst.getObjectDatabase().has(A_txt)); + assertEquals(B, dst.exactRef(master).getObjectId()); + fsck(dst, B); + } + + List<AccessEvent> requests = getRequests(); + assertEquals(enableProtocolV2 ? 3 : 2, requests.size()); + + assertFetchRequests(requests, 0); + } + + @Test public void testInitialClone_WithAuthenticationNoCredentials() throws Exception { try (Repository dst = createBareRepository(); 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 3d4df6b63c..a8b2e563f2 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -312,6 +312,8 @@ hoursAgo={0} hours ago httpConfigCannotNormalizeURL=Cannot normalize URL path {0}: too many .. segments httpConfigInvalidURL=Cannot parse URL from subsection http.{0} in git config; ignored. httpFactoryInUse=Changing the HTTP connection factory after an HTTP connection has already been opened is not allowed. +httpPreAuthTooLate=HTTP Basic preemptive authentication cannot be set once an HTTP connection has already been opened. +httpUserInfoDecodeError=Cannot decode user info from URL {}; ignored. httpWrongConnectionType=Wrong connection type: expected {0}, got {1}. hugeIndexesAreNotSupportedByJgitYet=Huge indexes are not supported by jgit, yet hunkBelongsToAnotherFile=Hunk belongs to another file 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 a62efa9894..07fb59ddf3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -340,6 +340,8 @@ public class JGitText extends TranslationBundle { /***/ public String httpConfigCannotNormalizeURL; /***/ public String httpConfigInvalidURL; /***/ public String httpFactoryInUse; + /***/ public String httpPreAuthTooLate; + /***/ public String httpUserInfoDecodeError; /***/ public String httpWrongConnectionType; /***/ public String hugeIndexesAreNotSupportedByJgitYet; /***/ public String hunkBelongsToAnotherFile; 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 1cad78b535..2e5d18dc15 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -41,6 +41,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.InterruptedIOException; import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.net.HttpCookie; import java.net.MalformedURLException; import java.net.Proxy; @@ -49,6 +50,7 @@ import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.InvalidPathException; import java.nio.file.Path; @@ -408,6 +410,41 @@ public class TransportHttp extends HttpTransport implements WalkTransport, return factory; } + /** + * Sets preemptive Basic HTTP authentication. If the given {@code username} + * or {@code password} is empty or {@code null}, no preemptive + * authentication will be done. If {@code username} and {@code password} are + * set, they will override authority information from the URI + * ("user:password@"). + * <p> + * If the connection encounters redirects, the pre-authentication will be + * cleared if the redirect goes to a different host. + * </p> + * + * @param username + * to use + * @param password + * to use + * @throws IllegalStateException + * if an HTTP/HTTPS connection has already been opened on this + * {@link TransportHttp} instance + * @since 5.11 + */ + public void setPreemptiveBasicAuthentication(String username, + String password) { + if (factoryUsed) { + throw new IllegalStateException(JGitText.get().httpPreAuthTooLate); + } + if (StringUtils.isEmptyOrNull(username) + || StringUtils.isEmptyOrNull(password)) { + authMethod = authFromUri(currentUri); + } else { + HttpAuthMethod basic = HttpAuthMethod.Type.BASIC.method(null); + basic.authorize(username, password); + authMethod = basic; + } + } + /** {@inheritDoc} */ @Override public FetchConnection openFetch() throws TransportException, @@ -563,6 +600,28 @@ public class TransportHttp extends HttpTransport implements WalkTransport, return new NoRemoteRepositoryException(u, text); } + private HttpAuthMethod authFromUri(URIish u) { + String user = u.getUser(); + String pass = u.getPass(); + if (user != null && pass != null) { + try { + // User/password are _not_ application/x-www-form-urlencoded. In + // particular the "+" sign would be replaced by a space. + user = URLDecoder.decode(user.replace("+", "%2B"), //$NON-NLS-1$ //$NON-NLS-2$ + StandardCharsets.UTF_8.name()); + pass = URLDecoder.decode(pass.replace("+", "%2B"), //$NON-NLS-1$ //$NON-NLS-2$ + StandardCharsets.UTF_8.name()); + HttpAuthMethod basic = HttpAuthMethod.Type.BASIC.method(null); + basic.authorize(user, pass); + return basic; + } catch (IllegalArgumentException + | UnsupportedEncodingException e) { + LOG.warn(JGitText.get().httpUserInfoDecodeError, u); + } + } + return HttpAuthMethod.Type.NONE.method(null); + } + private HttpConnection connect(String service) throws TransportException, NotSupportedException { return connect(service, null); @@ -572,6 +631,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport, TransferConfig.ProtocolVersion protocolVersion) throws TransportException, NotSupportedException { URL u = getServiceURL(service); + if (HttpAuthMethod.Type.NONE.equals(authMethod.getType())) { + authMethod = authFromUri(currentUri); + } int authAttempts = 1; int redirects = 0; Collection<Type> ignoreTypes = null; @@ -878,7 +940,13 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } try { URI redirectTo = new URI(location); + // Reset authentication if the redirect has user/password info or + // if the host is different. + boolean resetAuth = !StringUtils + .isEmptyOrNull(redirectTo.getUserInfo()); + String currentHost = currentUrl.getHost(); redirectTo = currentUrl.toURI().resolve(redirectTo); + resetAuth = resetAuth || !currentHost.equals(redirectTo.getHost()); String redirected = redirectTo.toASCIIString(); if (!isValidRedirect(baseUrl, redirected, checkFor)) { throw new TransportException(uri, @@ -887,6 +955,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } redirected = redirected.substring(0, redirected.indexOf(checkFor)); URIish result = new URIish(redirected); + if (resetAuth) { + authMethod = HttpAuthMethod.Type.NONE.method(null); + } if (LOG.isInfoEnabled()) { LOG.info(MessageFormat.format(JGitText.get().redirectHttp, uri.setPass(null), |