diff options
Diffstat (limited to 'org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java')
-rw-r--r-- | org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java | 1559 |
1 files changed, 1353 insertions, 206 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 1b6c552cef..b0d17adb3a 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 @@ -1,48 +1,16 @@ /* - * Copyright (C) 2010, Google Inc. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2010, 2020 Google Inc. and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.http.test; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_LENGTH; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE; @@ -50,169 +18,426 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.io.Writer; +import java.net.Proxy; +import java.net.URI; import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Collection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; - -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jgit.errors.RemoteRepositoryException; -import org.eclipse.jgit.errors.RepositoryNotFoundException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.ee10.servlet.FilterHolder; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.TransportConfigCallback; +import org.eclipse.jgit.errors.NoRemoteRepositoryException; 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.resolver.DefaultUploadPackFactory; import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRng; import org.eclipse.jgit.junit.http.AccessEvent; -import org.eclipse.jgit.junit.http.HttpTestCase; +import org.eclipse.jgit.junit.http.AppServer; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.AbstractAdvertiseRefsHook; +import org.eclipse.jgit.transport.AdvertiseRefsHook; +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.RefSpec; 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.UploadPack; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.transport.http.HttpConnection; import org.eclipse.jgit.transport.http.HttpConnectionFactory; -import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory; -import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory; -import org.eclipse.jgit.transport.resolver.RepositoryResolver; -import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.util.HttpSupport; +import org.eclipse.jgit.util.SystemReader; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; -@RunWith(Parameterized.class) -public class SmartClientSmartServerTest extends HttpTestCase { +public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase { private static final String HDR_TRANSFER_ENCODING = "Transfer-Encoding"; + private AdvertiseRefsHook advertiseRefsHook; + private Repository remoteRepository; + private CredentialsProvider testCredentials = new UsernamePasswordCredentialsProvider( + AppServer.username, AppServer.password); + private URIish remoteURI; private URIish brokenURI; - private RevBlob A_txt; + private URIish redirectURI; - private RevCommit A, B; + private URIish authURI; - @Parameters - public static Collection<Object[]> data() { - // run all tests with both connection factories we have - return Arrays.asList(new Object[][] { - { new JDKHttpConnectionFactory() }, - { new HttpClientConnectionFactory() } }); - } + private URIish authOnPostURI; + + private URIish slowURI; + + private URIish slowAuthURI; + + private RevBlob A_txt; + + private RevCommit A, B, unreachableCommit; - public SmartClientSmartServerTest(HttpConnectionFactory cf) { - HttpTransport.setConnectionFactory(cf); + public SmartClientSmartServerTest(TestParameters params) { + super(params); } + @Override @Before public void setUp() throws Exception { super.setUp(); final TestRepository<Repository> src = createTestRepository(); final String srcName = src.getRepository().getDirectory().getName(); - src.getRepository() - .getConfig() - .setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, - ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true); + StoredConfig cfg = src.getRepository().getConfig(); + cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true); + cfg.setInt("protocol", null, "version", enableProtocolV2 ? 2 : 0); + cfg.save(); - ServletContextHandler app = server.addContext("/git"); GitServlet gs = new GitServlet(); - gs.setRepositoryResolver(new RepositoryResolver<HttpServletRequest>() { - public Repository open(HttpServletRequest req, String name) - throws RepositoryNotFoundException, - ServiceNotEnabledException { - if (!name.equals(srcName)) - throw new RepositoryNotFoundException(name); - - final Repository db = src.getRepository(); - db.incrementOpen(); - return db; + gs.setUploadPackFactory((HttpServletRequest req, Repository db) -> { + DefaultUploadPackFactory f = new DefaultUploadPackFactory(); + UploadPack up = f.create(req, db); + if (advertiseRefsHook != null) { + up.setAdvertiseRefsHook(advertiseRefsHook); } + return up; }); + + ServletContextHandler app = addNormalContext(gs, src, srcName); + + ServletContextHandler broken = addBrokenContext(gs, srcName); + + ServletContextHandler redirect = addRedirectContext(gs); + + ServletContextHandler auth = addAuthContext(gs, "auth"); + + ServletContextHandler authOnPost = addAuthContext(gs, "pauth", "POST"); + + ServletContextHandler slow = addSlowContext(gs, "slow", false); + + ServletContextHandler slowAuth = addSlowContext(gs, "slowAuth", true); + + server.setUp(); + + remoteRepository = src.getRepository(); + remoteURI = toURIish(app, srcName); + brokenURI = toURIish(broken, srcName); + redirectURI = toURIish(redirect, srcName); + authURI = toURIish(auth, srcName); + authOnPostURI = toURIish(authOnPost, srcName); + slowURI = toURIish(slow, srcName); + slowAuthURI = toURIish(slowAuth, srcName); + + A_txt = src.blob("A"); + A = src.commit().add("A_txt", A_txt).create(); + B = src.commit().parent(A).add("A_txt", "C").add("B", "B").create(); + src.update(master, B); + + unreachableCommit = src.commit().add("A_txt", A_txt).create(); + + src.update("refs/garbage/a/very/long/ref/name/to/compress", B); + } + + private ServletContextHandler addNormalContext(GitServlet gs, TestRepository<Repository> src, String srcName) { + ServletContextHandler app = server.addContext("/git"); + app.addFilter(new FilterHolder(new Filter() { + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // empty + } + + // Does an internal forward for GET requests containing "/post/", + // and issues a 301 redirect on POST requests for such URLs. Used + // in the POST redirect tests. + @Override + public void doFilter(ServletRequest request, + ServletResponse response, FilterChain chain) + throws IOException, ServletException { + final HttpServletResponse httpServletResponse = (HttpServletResponse) response; + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + final StringBuffer fullUrl = httpServletRequest.getRequestURL(); + if (httpServletRequest.getQueryString() != null) { + fullUrl.append("?") + .append(httpServletRequest.getQueryString()); + } + String urlString = fullUrl.toString(); + if ("POST".equalsIgnoreCase(httpServletRequest.getMethod())) { + httpServletResponse.setStatus( + HttpServletResponse.SC_MOVED_PERMANENTLY); + httpServletResponse.setHeader(HttpSupport.HDR_LOCATION, + urlString.replace("/post/", "/")); + } else { + String path = httpServletRequest.getPathInfo(); + path = path.replace("/post/", "/"); + if (httpServletRequest.getQueryString() != null) { + path += '?' + httpServletRequest.getQueryString(); + } + RequestDispatcher dispatcher = httpServletRequest + .getRequestDispatcher(path); + dispatcher.forward(httpServletRequest, httpServletResponse); + } + } + + @Override + public void destroy() { + // empty + } + }), "/post/*", EnumSet.of(DispatcherType.REQUEST)); + gs.setRepositoryResolver(new TestRepositoryResolver(src, srcName)); app.addServlet(new ServletHolder(gs), "/*"); + return app; + } + private ServletContextHandler addBrokenContext(GitServlet gs, + String srcName) { ServletContextHandler broken = server.addContext("/bad"); broken.addFilter(new FilterHolder(new Filter() { + + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { final HttpServletResponse r = (HttpServletResponse) response; r.setContentType("text/plain"); - r.setCharacterEncoding("UTF-8"); - PrintWriter w = r.getWriter(); - w.print("OK"); - w.close(); + r.setCharacterEncoding(UTF_8.name()); + try (PrintWriter w = r.getWriter()) { + w.print("OK"); + } } - public void init(FilterConfig filterConfig) throws ServletException { - // + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // empty } + @Override public void destroy() { - // + // empty } }), "/" + srcName + "/git-upload-pack", EnumSet.of(DispatcherType.REQUEST)); broken.addServlet(new ServletHolder(gs), "/*"); + return broken; + } - server.setUp(); + private ServletContextHandler addAuthContext(GitServlet gs, + String contextPath, String... methods) { + ServletContextHandler auth = server.addContext('/' + contextPath); + auth.addServlet(new ServletHolder(gs), "/*"); + return server.authBasic(auth, methods); + } - remoteRepository = src.getRepository(); - remoteURI = toURIish(app, srcName); - brokenURI = toURIish(broken, srcName); + private ServletContextHandler addRedirectContext(GitServlet gs) { + ServletContextHandler redirect = server.addContext("/redirect"); + redirect.addFilter(new FilterHolder(new Filter() { + + // Enables tests for different codes, and for multiple redirects. + // First parameter is the number of redirects, second one is the + // redirect status code that should be used + 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 { + // empty + } - A_txt = src.blob("A"); - A = src.commit().add("A_txt", A_txt).create(); - B = src.commit().parent(A).add("A_txt", "C").add("B", "B").create(); - src.update(master, B); + private String local(String url, boolean toLocal) { + if (!toLocal) { + return url; + } + try { + URI u = new URI(url); + String fragment = u.getRawFragment(); + if (fragment != null) { + return u.getRawPath() + '#' + fragment; + } + return u.getRawPath(); + } catch (URISyntaxException e) { + return url; + } + } - src.update("refs/garbage/a/very/long/ref/name/to/compress", B); + @Override + public void doFilter(ServletRequest request, + ServletResponse response, FilterChain chain) + throws IOException, ServletException { + final HttpServletResponse httpServletResponse = (HttpServletResponse) response; + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + final StringBuffer fullUrl = httpServletRequest.getRequestURL(); + if (httpServletRequest.getQueryString() != null) { + fullUrl.append("?") + .append(httpServletRequest.getQueryString()); + } + String urlString = fullUrl.toString(); + boolean localRedirect = false; + if (urlString.contains("/local")) { + urlString = urlString.replace("/local", ""); + localRedirect = true; + } + if (urlString.contains("/loop/")) { + urlString = urlString.replace("/loop/", "/loop/x/"); + if (urlString.contains("/loop/x/x/x/x/x/x/x/x/")) { + // Go back to initial. + urlString = urlString.replace("/loop/x/x/x/x/x/x/x/x/", + "/loop/"); + } + httpServletResponse.setStatus( + HttpServletResponse.SC_MOVED_TEMPORARILY); + httpServletResponse.setHeader(HttpSupport.HDR_LOCATION, + local(urlString, localRedirect)); + return; + } + int responseCode = HttpServletResponse.SC_MOVED_PERMANENTLY; + int nofRedirects = 0; + Matcher matcher = responsePattern.matcher(urlString); + if (matcher.find()) { + nofRedirects = Integer + .parseUnsignedInt(matcher.group(1)); + responseCode = Integer.parseUnsignedInt(matcher.group(2)); + if (--nofRedirects <= 0) { + urlString = urlString.substring(0, matcher.start()) + + '/' + urlString.substring(matcher.end()); + } else { + urlString = urlString.substring(0, matcher.start()) + + "/response/" + nofRedirects + "/" + + responseCode + '/' + + urlString.substring(matcher.end()); + } + } + httpServletResponse.setStatus(responseCode); + if (nofRedirects <= 0) { + 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, + local(urlString, localRedirect)); + } + + @Override + public void destroy() { + // empty + } + }), "/*", EnumSet.of(DispatcherType.REQUEST)); + redirect.addServlet(new ServletHolder(gs), "/*"); + return redirect; + } + + private ServletContextHandler addSlowContext(GitServlet gs, String path, + boolean auth) { + ServletContextHandler slow = server.addContext('/' + path); + slow.addFilter(new FilterHolder(new Filter() { + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // empty + } + + // Simply delays the servlet for two seconds. Used for timeout + // tests, which use a one-second timeout. + @Override + public void doFilter(ServletRequest request, + ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + throw new IOException(e); + } + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // empty + } + }), "/*", EnumSet.of(DispatcherType.REQUEST)); + slow.addServlet(new ServletHolder(gs), "/*"); + if (auth) { + return server.authBasic(slow); + } + return slow; } @Test public void testListRemote() throws IOException { - Repository dst = createBareRepository(); - assertEquals("http", remoteURI.getScheme()); Map<String, Ref> map; - Transport t = Transport.open(dst, remoteURI); - try { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, remoteURI)) { // I didn't make up these public interface names, I just // approved them for inclusion into the code base. Sorry. // --spearce @@ -220,14 +445,9 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertTrue("isa TransportHttp", t instanceof TransportHttp); assertTrue("isa HttpTransport", t instanceof HttpTransport); - FetchConnection c = t.openFetch(); - try { + try (FetchConnection c = t.openFetch()) { map = c.getRefsMap(); - } finally { - c.close(); } - } finally { - t.close(); } assertNotNull("have map of refs", map); @@ -240,7 +460,7 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals(B, map.get(Constants.HEAD).getObjectId()); List<AccessEvent> requests = getRequests(); - assertEquals(1, requests.size()); + assertEquals(enableProtocolV2 ? 2 : 1, requests.size()); AccessEvent info = requests.get(0); assertEquals("GET", info.getMethod()); @@ -250,24 +470,37 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals(200, info.getStatus()); assertEquals("application/x-git-upload-pack-advertisement", info .getResponseHeader(HDR_CONTENT_TYPE)); - assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + if (!enableProtocolV2) { + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + } else { + AccessEvent lsRefs = requests.get(1); + assertEquals("POST", lsRefs.getMethod()); + assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath()); + assertEquals(0, lsRefs.getParameters().size()); + assertNotNull("has content-length", + lsRefs.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING)); + assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol")); + assertEquals(200, lsRefs.getStatus()); + assertEquals("application/x-git-upload-pack-result", + lsRefs.getResponseHeader(HDR_CONTENT_TYPE)); + } } @Test public void testListRemote_BadName() throws IOException, URISyntaxException { - Repository dst = createBareRepository(); URIish uri = new URIish(this.remoteURI.toString() + ".invalid"); - Transport t = Transport.open(dst, uri); - try { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, uri)) { try { t.openFetch(); fail("fetch connection opened"); - } catch (RemoteRepositoryException notFound) { - assertEquals(uri + ": Git repository not found", + } catch (NoRemoteRepositoryException notFound) { + assertEquals(uri + ": " + uri + + "/info/refs?service=git-upload-pack not found: Not Found", notFound.getMessage()); } - } finally { - t.close(); } List<AccessEvent> requests = getRequests(); @@ -278,31 +511,105 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals(join(uri, "info/refs"), info.getPath()); assertEquals(1, info.getParameters().size()); assertEquals("git-upload-pack", info.getParameter("service")); - assertEquals(200, info.getStatus()); + assertEquals(404, info.getStatus()); assertEquals("application/x-git-upload-pack-advertisement", info.getResponseHeader(HDR_CONTENT_TYPE)); } @Test - public void testInitialClone_Small() throws Exception { - Repository dst = createBareRepository(); - assertFalse(dst.hasObject(A_txt)); + public void testFetchBySHA1() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, remoteURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.fetch(NullProgressMonitor.INSTANCE, + Collections.singletonList(new RefSpec(B.name()))); + assertTrue(dst.getObjectDatabase().has(A_txt)); + } + } - Transport t = Transport.open(dst, remoteURI); - try { - t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); + @Test + public void testFetchBySHA1Unreachable() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, remoteURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + Exception e = assertThrows(TransportException.class, + () -> t.fetch(NullProgressMonitor.INSTANCE, + Collections.singletonList( + new RefSpec(unreachableCommit.name())))); + assertTrue(e.getMessage().contains( + "want " + unreachableCommit.name() + " not valid")); + } + assertLastRequestStatusCode(200); + } + + @Test + public void testFetchBySHA1UnreachableByAdvertiseRefsHook() + throws Exception { + advertiseRefsHook = new AbstractAdvertiseRefsHook() { + @Override + protected Map<String, Ref> getAdvertisedRefs(Repository repository, + RevWalk revWalk) { + return Collections.emptyMap(); + } + }; + + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, remoteURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + Exception e = assertThrows(TransportException.class, + () -> t.fetch(NullProgressMonitor.INSTANCE, + Collections.singletonList(new RefSpec(A.name())))); + assertTrue( + e.getMessage().contains("want " + A.name() + " not valid")); + } + assertLastRequestStatusCode(200); + } + + @Test + public void testTimeoutExpired() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, slowURI)) { + t.setTimeout(1); + TransportException expected = assertThrows(TransportException.class, + () -> t.fetch(NullProgressMonitor.INSTANCE, + mirror(master))); + assertTrue("Unexpected exception message: " + expected.toString(), + expected.getMessage().contains("time")); + } + } + + @Test + public void testTimeoutExpiredWithAuth() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, slowAuthURI)) { + t.setTimeout(1); + t.setCredentialsProvider(testCredentials); + TransportException expected = assertThrows(TransportException.class, + () -> t.fetch(NullProgressMonitor.INSTANCE, + mirror(master))); + assertTrue("Unexpected exception message: " + expected.toString(), + expected.getMessage().contains("time")); + assertFalse("Unexpected exception message: " + expected.toString(), + expected.getMessage().contains("auth")); } + } - assertTrue(dst.hasObject(A_txt)); - assertEquals(B, dst.getRef(master).getObjectId()); - fsck(dst, B); + @Test + public void testInitialClone_Small() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, remoteURI)) { + 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(2, requests.size()); + assertEquals(enableProtocolV2 ? 3 : 2, requests.size()); - AccessEvent info = requests.get(0); + int requestNumber = 0; + AccessEvent info = requests.get(requestNumber++); assertEquals("GET", info.getMethod()); assertEquals(join(remoteURI, "info/refs"), info.getPath()); assertEquals(1, info.getParameters().size()); @@ -310,9 +617,24 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals(200, info.getStatus()); assertEquals("application/x-git-upload-pack-advertisement", info .getResponseHeader(HDR_CONTENT_TYPE)); - assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + if (!enableProtocolV2) { + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + } else { + AccessEvent lsRefs = requests.get(requestNumber++); + assertEquals("POST", lsRefs.getMethod()); + assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath()); + assertEquals(0, lsRefs.getParameters().size()); + assertNotNull("has content-length", + lsRefs.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING)); + assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol")); + assertEquals(200, lsRefs.getStatus()); + assertEquals("application/x-git-upload-pack-result", + lsRefs.getResponseHeader(HDR_CONTENT_TYPE)); + } - AccessEvent service = requests.get(1); + AccessEvent service = requests.get(requestNumber); assertEquals("POST", service.getMethod()); assertEquals(join(remoteURI, "git-upload-pack"), service.getPath()); assertEquals(0, service.getParameters().size()); @@ -327,17 +649,691 @@ public class SmartClientSmartServerTest extends HttpTestCase { } @Test + public void test_CloneWithCustomFactory() throws Exception { + HttpConnectionFactory globalFactory = HttpTransport + .getConnectionFactory(); + HttpConnectionFactory failingConnectionFactory = new HttpConnectionFactory() { + + @Override + public HttpConnection create(URL url) throws IOException { + throw new IOException("Should not be reached"); + } + + @Override + public HttpConnection create(URL url, Proxy proxy) + throws IOException { + throw new IOException("Should not be reached"); + } + }; + HttpTransport.setConnectionFactory(failingConnectionFactory); + try { + File tmp = createTempDirectory("cloneViaApi"); + boolean[] localFactoryUsed = { false }; + TransportConfigCallback callback = new TransportConfigCallback() { + + @Override + public void configure(Transport transport) { + if (transport instanceof TransportHttp) { + ((TransportHttp) transport).setHttpConnectionFactory( + new HttpConnectionFactory() { + + @Override + public HttpConnection create(URL url) + throws IOException { + localFactoryUsed[0] = true; + return globalFactory.create(url); + } + + @Override + public HttpConnection create(URL url, + Proxy proxy) throws IOException { + localFactoryUsed[0] = true; + return globalFactory.create(url, proxy); + } + }); + } + } + }; + try (Git git = Git.cloneRepository().setDirectory(tmp) + .setTransportConfigCallback(callback) + .setURI(remoteURI.toPrivateString()).call()) { + assertTrue("Should have used the local HttpConnectionFactory", + localFactoryUsed[0]); + } + } finally { + HttpTransport.setConnectionFactory(globalFactory); + } + } + + private void initialClone_Redirect(int nofRedirects, int code) + throws Exception { + initialClone_Redirect(nofRedirects, code, false); + } + + private void initialClone_Redirect(int nofRedirects, int code, + boolean localRedirect) throws Exception { + URIish cloneFrom = redirectURI; + if (localRedirect) { + cloneFrom = extendPath(cloneFrom, "/local"); + } + if (code != 301 || nofRedirects > 1) { + cloneFrom = extendPath(cloneFrom, + "/response/" + nofRedirects + "/" + code); + } + + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, cloneFrom)) { + 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) + nofRedirects, + requests.size()); + + int n = 0; + while (n < nofRedirects) { + AccessEvent redirect = requests.get(n++); + assertEquals(code, redirect.getStatus()); + } + + AccessEvent info = requests.get(n++); + assertEquals("GET", info.getMethod()); + assertEquals(join(remoteURI, "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)); + if (!enableProtocolV2) { + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + } else { + AccessEvent lsRefs = requests.get(n++); + assertEquals("POST", lsRefs.getMethod()); + assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath()); + assertEquals(0, lsRefs.getParameters().size()); + assertNotNull("has content-length", + lsRefs.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING)); + assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol")); + assertEquals(200, lsRefs.getStatus()); + assertEquals("application/x-git-upload-pack-result", + lsRefs.getResponseHeader(HDR_CONTENT_TYPE)); + } + + AccessEvent service = requests.get(n++); + assertEquals("POST", service.getMethod()); + assertEquals(join(remoteURI, "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_Redirect301Small() throws Exception { + initialClone_Redirect(1, 301); + } + + @Test + public void testInitialClone_Redirect301Local() throws Exception { + initialClone_Redirect(1, 301, true); + } + + @Test + public void testInitialClone_Redirect302Small() throws Exception { + initialClone_Redirect(1, 302); + } + + @Test + public void testInitialClone_Redirect303Small() throws Exception { + initialClone_Redirect(1, 303); + } + + @Test + public void testInitialClone_Redirect307Small() throws Exception { + initialClone_Redirect(1, 307); + } + + @Test + public void testInitialClone_RedirectMultiple() throws Exception { + initialClone_Redirect(4, 302); + } + + @Test + public void testInitialClone_RedirectMax() throws Exception { + StoredConfig userConfig = SystemReader.getInstance() + .getUserConfig(); + userConfig.setInt("http", null, "maxRedirects", 4); + userConfig.save(); + initialClone_Redirect(4, 302); + } + + @Test + public void testInitialClone_RedirectTooOften() throws Exception { + StoredConfig userConfig = SystemReader.getInstance() + .getUserConfig(); + userConfig.setInt("http", null, "maxRedirects", 3); + userConfig.save(); + + URIish cloneFrom = extendPath(redirectURI, "/response/4/302"); + String remoteUri = cloneFrom.toString(); + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, cloneFrom)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + fail("Should have failed (too many redirects)"); + } catch (TransportException e) { + String expectedMessageBegin = remoteUri.toString() + ": " + + MessageFormat.format(JGitText.get().redirectLimitExceeded, + "3", remoteUri.replace("/4/", "/1/") + '/', ""); + String message = e.getMessage(); + if (message.length() > expectedMessageBegin.length()) { + message = message.substring(0, expectedMessageBegin.length()); + } + assertEquals(expectedMessageBegin, message); + } + } + + @Test + public void testInitialClone_RedirectLoop() throws Exception { + URIish cloneFrom = extendPath(redirectURI, "/loop"); + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, cloneFrom)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + fail("Should have failed (redirect loop)"); + } catch (TransportException e) { + assertTrue(e.getMessage().contains("Redirected more than")); + } + } + + @Test + public void testInitialClone_RedirectOnPostAllowed() throws Exception { + StoredConfig userConfig = SystemReader.getInstance() + .getUserConfig(); + userConfig.setString("http", null, "followRedirects", "true"); + userConfig.save(); + + URIish cloneFrom = extendPath(remoteURI, "/post"); + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, cloneFrom)) { + 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 ? 4 : 3, requests.size()); + + AccessEvent info = requests.get(0); + assertEquals("GET", info.getMethod()); + assertEquals(join(cloneFrom, "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)); + if (!enableProtocolV2) { + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + } + + AccessEvent redirect = requests.get(1); + assertEquals("POST", redirect.getMethod()); + assertEquals(301, redirect.getStatus()); + + for (int i = 2; i < requests.size(); i++) { + AccessEvent service = requests.get(i); + assertEquals("POST", service.getMethod()); + assertEquals(join(remoteURI, "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_RedirectOnPostForbidden() throws Exception { + URIish cloneFrom = extendPath(remoteURI, "/post"); + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, cloneFrom)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + fail("Should have failed (redirect on POST)"); + } catch (TransportException e) { + assertTrue(e.getMessage().contains("301")); + } + assertLastRequestStatusCode(301); + } + + @Test + public void testInitialClone_RedirectForbidden() throws Exception { + StoredConfig userConfig = SystemReader.getInstance() + .getUserConfig(); + userConfig.setString("http", null, "followRedirects", "false"); + userConfig.save(); + + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, redirectURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + fail("Should have failed (redirects forbidden)"); + } catch (TransportException e) { + assertTrue( + e.getMessage().contains("http.followRedirects is false")); + } + assertLastRequestStatusCode(301); + } + + 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()); + assertEquals("git-upload-pack", info.getParameter("service")); + assertEquals(200, info.getStatus()); + assertEquals("application/x-git-upload-pack-advertisement", + info.getResponseHeader(HDR_CONTENT_TYPE)); + if (!enableProtocolV2) { + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + } + + 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()); + 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_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(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + 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 { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + 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 { + 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 (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, cloneFrom)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); + t.setCredentialsProvider(uriSpecificCredentialsProvider); + 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 ? 5 : 4, requests.size()); + + int requestNumber = 0; + AccessEvent redirect = requests.get(requestNumber++); + assertEquals("GET", redirect.getMethod()); + assertEquals(join(cloneFrom, "info/refs"), redirect.getPath()); + assertEquals(301, redirect.getStatus()); + + AccessEvent info = requests.get(requestNumber++); + assertEquals("GET", info.getMethod()); + assertEquals(join(authURI, "info/refs"), info.getPath()); + assertEquals(401, info.getStatus()); + + info = requests.get(requestNumber++); + 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)); + if (!enableProtocolV2) { + assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); + } else { + AccessEvent lsRefs = requests.get(requestNumber++); + assertEquals("POST", lsRefs.getMethod()); + assertEquals(join(authURI, "git-upload-pack"), lsRefs.getPath()); + assertEquals(0, lsRefs.getParameters().size()); + assertNotNull("has content-length", + lsRefs.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING)); + assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol")); + assertEquals(200, lsRefs.getStatus()); + assertEquals("application/x-git-upload-pack-result", + lsRefs.getResponseHeader(HDR_CONTENT_TYPE)); + } + + AccessEvent service = requests.get(requestNumber); + 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 { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, authOnPostURI)) { + 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(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)); + if (!enableProtocolV2) { + 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()); + + for (int i = 2; i < requests.size(); i++) { + service = requests.get(i); + 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. // TestRepository dst = createTestRepository(); - Transport t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } - assertEquals(B, dst.getRepository().getRef(master).getObjectId()); + assertEquals(B, dst.getRepository().exactRef(master).getObjectId()); List<AccessEvent> cloneRequests = getRequests(); // Only create a few new commits. @@ -347,24 +1343,27 @@ public class SmartClientSmartServerTest extends HttpTestCase { // Create a new commit on the remote. // - b = new TestRepository<Repository>(remoteRepository).branch(master); - RevCommit Z = b.commit().message("Z").create(); + RevCommit Z; + try (TestRepository<Repository> tr = new TestRepository<>( + remoteRepository)) { + b = tr.branch(master); + Z = b.commit().message("Z").create(); + } // Now incrementally update. // - t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } - assertEquals(Z, dst.getRepository().getRef(master).getObjectId()); + assertEquals(Z, dst.getRepository().exactRef(master).getObjectId()); List<AccessEvent> requests = getRequests(); requests.removeAll(cloneRequests); - assertEquals(2, requests.size()); - AccessEvent info = requests.get(0); + assertEquals(enableProtocolV2 ? 3 : 2, requests.size()); + + int requestNumber = 0; + AccessEvent info = requests.get(requestNumber++); assertEquals("GET", info.getMethod()); assertEquals(join(remoteURI, "info/refs"), info.getPath()); assertEquals(1, info.getParameters().size()); @@ -373,9 +1372,24 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals("application/x-git-upload-pack-advertisement", info.getResponseHeader(HDR_CONTENT_TYPE)); + if (enableProtocolV2) { + AccessEvent lsRefs = requests.get(requestNumber++); + assertEquals("POST", lsRefs.getMethod()); + assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath()); + assertEquals(0, lsRefs.getParameters().size()); + assertNotNull("has content-length", + lsRefs.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING)); + assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol")); + assertEquals(200, lsRefs.getStatus()); + assertEquals("application/x-git-upload-pack-result", + lsRefs.getResponseHeader(HDR_CONTENT_TYPE)); + } + // We should have needed one request to perform the fetch. // - AccessEvent service = requests.get(1); + AccessEvent service = requests.get(requestNumber); assertEquals("POST", service.getMethod()); assertEquals(join(remoteURI, "git-upload-pack"), service.getPath()); assertEquals(0, service.getParameters().size()); @@ -394,13 +1408,10 @@ public class SmartClientSmartServerTest extends HttpTestCase { // Bootstrap by doing the clone. // TestRepository dst = createTestRepository(); - Transport t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } - assertEquals(B, dst.getRepository().getRef(master).getObjectId()); + assertEquals(B, dst.getRepository().exactRef(master).getObjectId()); List<AccessEvent> cloneRequests = getRequests(); // Force enough into the local client that enumeration will @@ -413,24 +1424,26 @@ public class SmartClientSmartServerTest extends HttpTestCase { // Create a new commit on the remote. // - b = new TestRepository<Repository>(remoteRepository).branch(master); - RevCommit Z = b.commit().message("Z").create(); + RevCommit Z; + try (TestRepository<Repository> tr = new TestRepository<>( + remoteRepository)) { + b = tr.branch(master); + Z = b.commit().message("Z").create(); + } // Now incrementally update. // - t = Transport.open(dst.getRepository(), remoteURI); - try { + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); - } finally { - t.close(); } - assertEquals(Z, dst.getRepository().getRef(master).getObjectId()); + assertEquals(Z, dst.getRepository().exactRef(master).getObjectId()); List<AccessEvent> requests = getRequests(); requests.removeAll(cloneRequests); - assertEquals(3, requests.size()); + assertEquals(enableProtocolV2 ? 4 : 3, requests.size()); - AccessEvent info = requests.get(0); + int requestNumber = 0; + AccessEvent info = requests.get(requestNumber++); assertEquals("GET", info.getMethod()); assertEquals(join(remoteURI, "info/refs"), info.getPath()); assertEquals(1, info.getParameters().size()); @@ -439,10 +1452,25 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals("application/x-git-upload-pack-advertisement", info .getResponseHeader(HDR_CONTENT_TYPE)); + if (enableProtocolV2) { + AccessEvent lsRefs = requests.get(requestNumber++); + assertEquals("POST", lsRefs.getMethod()); + assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath()); + assertEquals(0, lsRefs.getParameters().size()); + assertNotNull("has content-length", + lsRefs.getRequestHeader(HDR_CONTENT_LENGTH)); + assertNull("not chunked", + lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING)); + assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol")); + assertEquals(200, lsRefs.getStatus()); + assertEquals("application/x-git-upload-pack-result", + lsRefs.getResponseHeader(HDR_CONTENT_TYPE)); + } + // We should have needed two requests to perform the fetch // due to the high number of local unknown commits. // - AccessEvent service = requests.get(1); + AccessEvent service = requests.get(requestNumber++); assertEquals("POST", service.getMethod()); assertEquals(join(remoteURI, "git-upload-pack"), service.getPath()); assertEquals(0, service.getParameters().size()); @@ -455,7 +1483,7 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals("application/x-git-upload-pack-result", service .getResponseHeader(HDR_CONTENT_TYPE)); - service = requests.get(2); + service = requests.get(requestNumber); assertEquals("POST", service.getMethod()); assertEquals(join(remoteURI, "git-upload-pack"), service.getPath()); assertEquals(0, service.getParameters().size()); @@ -470,23 +1498,77 @@ public class SmartClientSmartServerTest extends HttpTestCase { } @Test - public void testInitialClone_BrokenServer() throws Exception { - Repository dst = createBareRepository(); - assertFalse(dst.hasObject(A_txt)); + public void testFetch_MaxHavesCutoffAfterAckOnly() throws Exception { + // Bootstrap by doing the clone. + // + TestRepository dst = createTestRepository(); + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { + t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); + } + assertEquals(B, dst.getRepository().exactRef(master).getObjectId()); - Transport t = Transport.open(dst, brokenURI); - try { + // Force enough into the local client that enumeration will + // need more than MAX_HAVES (256) haves to be sent. The server + // doesn't know any of these, so it will never ACK. The client + // should keep going. + // + // If it does, client and server will find a common commit, and + // the pack file will contain exactly the one commit object Z + // we create on the remote, which we can test for via the progress + // monitor, which should have something like + // "Receiving objects: 100% (1/1)". If the client sends a "done" + // too early, the server will send more objects, and we'll have + // a line like "Receiving objects: 100% (8/8)". + TestRepository.BranchBuilder b = dst.branch(master); + // The client will send 32 + 64 + 128 + 256 + 512 haves. Only the + // last one will be a common commit. If the cutoff kicks in too + // early (after 480), we'll get too many objects in the fetch. + for (int i = 0; i < 992; i++) + b.commit().tick(3600 /* 1 hour */).message("c" + i).create(); + + // Create a new commit on the remote. + // + RevCommit Z; + try (TestRepository<Repository> tr = new TestRepository<>( + remoteRepository)) { + b = tr.branch(master); + Z = b.commit().message("Z").create(); + } + + // Now incrementally update. + // + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(buffer, StandardCharsets.UTF_8); + TextProgressMonitor monitor = new TextProgressMonitor(writer); + try (Transport t = Transport.open(dst.getRepository(), remoteURI)) { + t.fetch(monitor, mirror(master)); + } + assertEquals(Z, dst.getRepository().exactRef(master).getObjectId()); + + String progressMessages = new String(buffer.toByteArray(), + StandardCharsets.UTF_8); + Pattern expected = Pattern + .compile("Receiving objects:\\s+100% \\(1/1\\)\n"); + if (!expected.matcher(progressMessages).find()) { + System.out.println(progressMessages); + fail("Expected only one object to be sent"); + } + } + + @Test + public void testInitialClone_BrokenServer() throws Exception { + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, brokenURI)) { + assertFalse(dst.getObjectDatabase().has(A_txt)); try { t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); fail("fetch completed despite upload-pack being broken"); } catch (TransportException err) { String exp = brokenURI + ": expected" + " Content-Type application/x-git-upload-pack-result;" - + " received Content-Type text/plain; charset=UTF-8"; + + " received Content-Type text/plain;charset=utf-8"; assertEquals(exp, err.getMessage()); } - } finally { - t.close(); } List<AccessEvent> requests = getRequests(); @@ -506,23 +1588,92 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals(join(brokenURI, "git-upload-pack"), service.getPath()); assertEquals(0, service.getParameters().size()); assertEquals(200, service.getStatus()); - assertEquals("text/plain; charset=UTF-8", + assertEquals("text/plain;charset=utf-8", service.getResponseHeader(HDR_CONTENT_TYPE)); } @Test + public void testInvalidWant() throws Exception { + ObjectId id; + try (ObjectInserter.Formatter formatter = new ObjectInserter.Formatter()) { + id = formatter.idFor(Constants.OBJ_BLOB, + "testInvalidWant".getBytes(UTF_8)); + } + + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, remoteURI); + FetchConnection c = t.openFetch()) { + Ref want = new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, id.name(), + id); + c.fetch(NullProgressMonitor.INSTANCE, Collections.singleton(want), + Collections.<ObjectId> emptySet()); + fail("Server accepted want " + id.name()); + } catch (TransportException err) { + assertTrue(err.getMessage() + .contains("want " + id.name() + " not valid")); + } + assertLastRequestStatusCode(200); + } + + @Test + public void testFetch_RefsUnreadableOnUpload() throws Exception { + AppServer noRefServer = new AppServer(); + try { + final String repoName = "refs-unreadable"; + RefsUnreadableInMemoryRepository badRefsRepo = new RefsUnreadableInMemoryRepository( + new DfsRepositoryDescription(repoName)); + final TestRepository<Repository> repo = new TestRepository<>( + badRefsRepo); + badRefsRepo.getConfig().setInt("protocol", null, "version", + enableProtocolV2 ? 2 : 0); + + ServletContextHandler app = noRefServer.addContext("/git"); + GitServlet gs = new GitServlet(); + gs.setRepositoryResolver(new TestRepositoryResolver(repo, repoName)); + app.addServlet(new ServletHolder(gs), "/*"); + noRefServer.setUp(); + + RevBlob A2_txt = repo.blob("A2"); + RevCommit A2 = repo.commit().add("A2_txt", A2_txt).create(); + RevCommit B2 = repo.commit().parent(A2).add("A2_txt", "C2") + .add("B2", "B2").create(); + repo.update(master, B2); + + URIish badRefsURI = new URIish(noRefServer.getURI() + .resolve(app.getContextPath() + "/" + repoName).toString()); + + try (Repository dst = createBareRepository(); + Transport t = Transport.open(dst, badRefsURI); + FetchConnection c = t.openFetch()) { + // We start failing here to exercise the post-advertisement + // upload pack handler. + badRefsRepo.startFailing(); + // Need to flush caches because ref advertisement populated them. + badRefsRepo.getRefDatabase().refresh(); + c.fetch(NullProgressMonitor.INSTANCE, + Collections.singleton(c.getRef(master)), + Collections.<ObjectId> emptySet()); + fail("Successfully served ref with value " + c.getRef(master)); + } catch (TransportException err) { + assertTrue("Unexpected exception message " + err.getMessage(), + err.getMessage().contains("Server Error")); + } + } finally { + noRefServer.tearDown(); + } + } + + @Test public void testPush_NotAuthorized() throws Exception { final TestRepository src = createTestRepository(); final RevBlob Q_txt = src.blob("new text"); final RevCommit Q = src.commit().add("Q", Q_txt).create(); final Repository db = src.getRepository(); final String dstName = Constants.R_HEADS + "new.branch"; - Transport t; // push anonymous shouldn't be allowed. // - t = Transport.open(db, remoteURI); - try { + try (Transport t = Transport.open(db, remoteURI)) { final String srcExpr = Q.name(); final boolean forceUpdate = false; final String localName = null; @@ -538,8 +1689,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { + JGitText.get().authenticationNotSupported; assertEquals(exp, e.getMessage()); } - } finally { - t.close(); } List<AccessEvent> requests = getRequests(); @@ -560,12 +1709,10 @@ public class SmartClientSmartServerTest extends HttpTestCase { final RevCommit Q = src.commit().add("Q", Q_txt).create(); final Repository db = src.getRepository(); final String dstName = Constants.R_HEADS + "new.branch"; - Transport t; enableReceivePack(); - t = Transport.open(db, remoteURI); - try { + try (Transport t = Transport.open(db, remoteURI)) { final String srcExpr = Q.name(); final boolean forceUpdate = false; final String localName = null; @@ -574,17 +1721,16 @@ public class SmartClientSmartServerTest extends HttpTestCase { RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(), srcExpr, dstName, forceUpdate, localName, oldId); t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u)); - } finally { - t.close(); } - assertTrue(remoteRepository.hasObject(Q_txt)); - assertNotNull("has " + dstName, remoteRepository.getRef(dstName)); - assertEquals(Q, remoteRepository.getRef(dstName).getObjectId()); + assertTrue(remoteRepository.getObjectDatabase().has(Q_txt)); + assertNotNull("has " + dstName, remoteRepository.exactRef(dstName)); + assertEquals(Q, remoteRepository.exactRef(dstName).getObjectId()); fsck(remoteRepository, Q); - final ReflogReader log = remoteRepository.getReflogReader(dstName); - assertNotNull("has log for " + dstName); + final ReflogReader log = remoteRepository.getRefDatabase() + .getReflogReader(dstName); + assertNotNull("has log for " + dstName, log); final ReflogEntry last = log.getLastEntry(); assertNotNull("has last entry", last); @@ -633,7 +1779,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { final RevCommit Q = src.commit().add("Q", Q_bin).create(); final Repository db = src.getRepository(); final String dstName = Constants.R_HEADS + "new.branch"; - Transport t; enableReceivePack(); @@ -642,8 +1787,7 @@ public class SmartClientSmartServerTest extends HttpTestCase { cfg.setInt("http", null, "postbuffer", 8 * 1024); cfg.save(); - t = Transport.open(db, remoteURI); - try { + try (Transport t = Transport.open(db, remoteURI)) { final String srcExpr = Q.name(); final boolean forceUpdate = false; final String localName = null; @@ -652,13 +1796,11 @@ public class SmartClientSmartServerTest extends HttpTestCase { RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(), srcExpr, dstName, forceUpdate, localName, oldId); t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u)); - } finally { - t.close(); } - assertTrue(remoteRepository.hasObject(Q_bin)); - assertNotNull("has " + dstName, remoteRepository.getRef(dstName)); - assertEquals(Q, remoteRepository.getRef(dstName).getObjectId()); + assertTrue(remoteRepository.getObjectDatabase().has(Q_bin)); + assertNotNull("has " + dstName, remoteRepository.exactRef(dstName)); + assertEquals(Q, remoteRepository.exactRef(dstName).getObjectId()); fsck(remoteRepository, Q); List<AccessEvent> requests = getRequests(); @@ -686,6 +1828,11 @@ public class SmartClientSmartServerTest extends HttpTestCase { .getResponseHeader(HDR_CONTENT_TYPE)); } + private void assertLastRequestStatusCode(int statusCode) { + List<AccessEvent> requests = getRequests(); + assertEquals(statusCode, requests.get(requests.size() - 1).getStatus()); + } + private void enableReceivePack() throws IOException { final StoredConfig cfg = remoteRepository.getConfig(); cfg.setBoolean("http", null, "receivepack", true); |