Browse Source

Add support to follow HTTP redirects

git-core follows HTTP redirects so JGit should also provide this.

Implement config setting http.followRedirects with possible values
"false" (= never), "true" (= always), and "initial" (only on GET, but
not on POST).[1]

We must do our own redirect handling and cannot rely on the support
that the underlying real connection may offer. At least the JDK's
HttpURLConnection has two features that get in the way:

* it does not allow cross-protocol redirects and thus fails on
  http->https redirects (for instance, on Github).
* it translates a redirect after a POST to a GET unless the system
  property "http.strictPostRedirect" is set to true. We don't want
  to manipulate that system setting nor require it.

Additionally, git has its own rules about what redirects it accepts;[2]
for instance, it does not allow a redirect that adds query arguments.

We handle response codes 301, 302, 303, and 307 as per RFC 2616.[3]
On POST we do not handle 303, and we follow redirects only if
http.followRedirects == true.

Redirects are followed only a certain number of times. There are two
ways to control that limit:

* by default, the limit is given by the http.maxRedirects system
  property that is also used by the JDK. If the system property is
  not set, the default is 5. (This is much lower than the JDK default
  of 20, but I don't see the value of following so many redirects.)
* this can be overwritten by a http.maxRedirects git config setting.

The JGit http.* git config settings are currently all global; JGit has
no support yet for URI-specific settings "http.<pattern>.name". Adding
support for that is well beyond the scope of this change.

Like git-core, we log every redirect attempt (LOG.info) so that users
may know about the redirection having occurred.

Extends the test framework to configure an AppServer with HTTPS support
so that we can test cloning via HTTPS and redirections involving HTTPS.

[1] https://git-scm.com/docs/git-config
[2] 6628eb41db
[3] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

CQ: 13987
Bug: 465167
Change-Id: I86518cb76842f7d326b51f8715e3bbf8ada89859
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
stable-4.9
Thomas Wolf 10 years ago committed by Matthias Sohn
parent
commit
e17bfc96f2
  1. 3
      org.eclipse.jgit.http.test/pom.xml
  2. 85
      org.eclipse.jgit.http.test/src/org/eclipse/jgit/http/test/TestRepositoryResolver.java
  3. 21
      org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java
  4. 280
      org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerSslTest.java
  5. 311
      org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
  6. 1
      org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
  7. 136
      org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
  8. 51
      org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java
  9. 20
      org.eclipse.jgit/.settings/.api_filters
  10. 7
      org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
  11. 7
      org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
  12. 304
      org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
  13. 24
      org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java

3
org.eclipse.jgit.http.test/pom.xml

@ -87,14 +87,12 @@
<groupId>org.eclipse.jgit</groupId> <groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.junit.http</artifactId> <artifactId>org.eclipse.jgit.junit.http</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.eclipse.jgit</groupId> <groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.junit</artifactId> <artifactId>org.eclipse.jgit.junit</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -107,7 +105,6 @@
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId> <artifactId>jetty-servlet</artifactId>
<scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>

85
org.eclipse.jgit.http.test/src/org/eclipse/jgit/http/test/TestRepositoryResolver.java

@ -0,0 +1,85 @@
/*
* Copyright (C) 2016, 2017 Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* 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
*
* 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.
*/
package org.eclipse.jgit.http.test;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.resolver.RepositoryResolver;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
/** A simple repository resolver for tests. */
public final class TestRepositoryResolver
implements RepositoryResolver<HttpServletRequest> {
private final TestRepository<Repository> repo;
private final String repoName;
/**
* Creates a new {@link TestRepositoryResolver} that resolves the given name to
* the given repository.
*
* @param repo
* to resolve to
* @param repoName
* to match
*/
public TestRepositoryResolver(TestRepository<Repository> repo, String repoName) {
this.repo = repo;
this.repoName = repoName;
}
@Override
public Repository open(HttpServletRequest req, String name)
throws RepositoryNotFoundException, ServiceNotEnabledException {
if (!name.equals(repoName)) {
throw new RepositoryNotFoundException(name);
}
Repository db = repo.getRepository();
db.incrementOpen();
return db;
}
}

21
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2010, Google Inc. * Copyright (C) 2010, 2017 Google Inc.
* and other copyright owners as documented in the project's IP log. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -60,12 +60,9 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jgit.errors.NotSupportedException; import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.http.server.GitServlet; import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.http.AccessEvent; import org.eclipse.jgit.junit.http.AccessEvent;
@ -84,8 +81,6 @@ import org.eclipse.jgit.transport.URIish;
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;
import org.eclipse.jgit.transport.resolver.RepositoryResolver;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -124,19 +119,7 @@ public class DumbClientSmartServerTest extends HttpTestCase {
ServletContextHandler app = server.addContext("/git"); ServletContextHandler app = server.addContext("/git");
GitServlet gs = new GitServlet(); GitServlet gs = new GitServlet();
gs.setRepositoryResolver(new RepositoryResolver<HttpServletRequest>() { gs.setRepositoryResolver(new TestRepositoryResolver(src, srcName));
@Override
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;
}
});
app.addServlet(new ServletHolder(gs), "/*"); app.addServlet(new ServletHolder(gs), "/*");
server.setUp(); server.setUp();

280
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerSslTest.java

@ -0,0 +1,280 @@
/*
* Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch>
* and other copyright owners as documented in the project's IP log.
*
* 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
*
* 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.
*/
package org.eclipse.jgit.http.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
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.TransportException;
import org.eclipse.jgit.http.server.GitServlet;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.junit.http.AccessEvent;
import org.eclipse.jgit.junit.http.AppServer;
import org.eclipse.jgit.junit.http.HttpTestCase;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevBlob;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.transport.HttpTransport;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
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.util.FS;
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 SmartClientSmartServerSslTest extends HttpTestCase {
private URIish remoteURI;
private URIish secureURI;
private RevBlob A_txt;
private RevCommit A, B;
@Parameters
public static Collection<Object[]> data() {
// run all tests with both connection factories we have
return Arrays.asList(new Object[][] {
{ new JDKHttpConnectionFactory() },
{ new HttpClientConnectionFactory() } });
}
public SmartClientSmartServerSslTest(HttpConnectionFactory cf) {
HttpTransport.setConnectionFactory(cf);
}
@Override
protected AppServer createServer() {
return new AppServer(0, 0);
}
@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);
GitServlet gs = new GitServlet();
ServletContextHandler app = addNormalContext(gs, src, srcName);
server.setUp();
remoteURI = toURIish(app, srcName);
secureURI = new URIish(rewriteUrl(remoteURI.toString(), "https",
server.getSecurePort()));
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);
src.update("refs/garbage/a/very/long/ref/name/to/compress", B);
FileBasedConfig userConfig = SystemReader.getInstance()
.openUserConfig(null, FS.DETECTED);
userConfig.setBoolean("http", null, "sslVerify", false);
userConfig.save();
}
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
}
// Redirects http to https for requests containing "/https/".
@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 = rewriteUrl(fullUrl.toString(), "https",
server.getSecurePort());
httpServletResponse
.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
httpServletResponse.setHeader(HttpSupport.HDR_LOCATION,
urlString.replace("/https/", "/"));
}
@Override
public void destroy() {
// empty
}
}), "/https/*", EnumSet.of(DispatcherType.REQUEST));
app.addFilter(new FilterHolder(new Filter() {
@Override
public void init(FilterConfig filterConfig)
throws ServletException {
// empty
}
// Redirects https back to http for requests containing "/back/".
@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 = rewriteUrl(fullUrl.toString(), "http",
server.getPort());
httpServletResponse
.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
httpServletResponse.setHeader(HttpSupport.HDR_LOCATION,
urlString.replace("/back/", "/"));
}
@Override
public void destroy() {
// empty
}
}), "/back/*", EnumSet.of(DispatcherType.REQUEST));
gs.setRepositoryResolver(new TestRepositoryResolver(src, srcName));
app.addServlet(new ServletHolder(gs), "/*");
return app;
}
@Test
public void testInitialClone_ViaHttps() throws Exception {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
try (Transport t = Transport.open(dst, secureURI)) {
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(2, requests.size());
}
@Test
public void testInitialClone_RedirectToHttps() throws Exception {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(remoteURI, "/https");
try (Transport t = Transport.open(dst, cloneFrom)) {
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());
}
@Test
public void testInitialClone_RedirectBackToHttp() throws Exception {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(secureURI, "/back");
try (Transport t = Transport.open(dst, cloneFrom)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
fail("Should have failed (redirect from https to http)");
} catch (TransportException e) {
assertTrue(e.getMessage().contains("not allowed"));
}
}
}

311
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2010, Google Inc. * Copyright (C) 2010, 2017 Google Inc.
* and other copyright owners as documented in the project's IP log. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -57,17 +57,21 @@ import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.DispatcherType; import javax.servlet.DispatcherType;
import javax.servlet.Filter; import javax.servlet.Filter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
import javax.servlet.FilterConfig; import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
@ -78,7 +82,6 @@ import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
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.RepositoryNotFoundException;
import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.TransportException;
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;
@ -101,6 +104,7 @@ import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.lib.StoredConfig;
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.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;
@ -110,9 +114,9 @@ import org.eclipse.jgit.transport.URIish;
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;
import org.eclipse.jgit.transport.resolver.RepositoryResolver; import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
import org.eclipse.jgit.util.HttpSupport; import org.eclipse.jgit.util.HttpSupport;
import org.eclipse.jgit.util.SystemReader;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -184,7 +188,52 @@ public class SmartClientSmartServerTest extends HttpTestCase {
private ServletContextHandler addNormalContext(GitServlet gs, TestRepository<Repository> src, String srcName) { private ServletContextHandler addNormalContext(GitServlet gs, TestRepository<Repository> src, String srcName) {
ServletContextHandler app = server.addContext("/git"); ServletContextHandler app = server.addContext("/git");
gs.setRepositoryResolver(new TestRepoResolver(src, srcName)); 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), "/*"); app.addServlet(new ServletHolder(gs), "/*");
return app; return app;
} }
@ -228,6 +277,12 @@ public class SmartClientSmartServerTest extends HttpTestCase {
ServletContextHandler redirect = server.addContext("/redirect"); ServletContextHandler redirect = server.addContext("/redirect");
redirect.addFilter(new FilterHolder(new Filter() { 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])/");
@Override @Override
public void init(FilterConfig filterConfig) public void init(FilterConfig filterConfig)
throws ServletException { throws ServletException {
@ -245,10 +300,43 @@ public class SmartClientSmartServerTest extends HttpTestCase {
fullUrl.append("?") fullUrl.append("?")
.append(httpServletRequest.getQueryString()); .append(httpServletRequest.getQueryString());
} }
httpServletResponse String urlString = fullUrl.toString();
.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); 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,
urlString);
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 = fullUrl.substring(0, matcher.start()) + '/'
+ fullUrl.substring(matcher.end());
} else {
urlString = fullUrl.substring(0, matcher.start())
+ "/response/" + nofRedirects + "/"
+ responseCode + '/'
+ fullUrl.substring(matcher.end());
}
}
httpServletResponse.setStatus(responseCode);
if (nofRedirects <= 0) {
urlString = urlString.replace("/redirect", "/git");
}
httpServletResponse.setHeader(HttpSupport.HDR_LOCATION, httpServletResponse.setHeader(HttpSupport.HDR_LOCATION,
fullUrl.toString().replace("/redirect", "/git")); urlString);
} }
@Override @Override
@ -373,12 +461,17 @@ public class SmartClientSmartServerTest extends HttpTestCase {
.getResponseHeader(HDR_CONTENT_TYPE)); .getResponseHeader(HDR_CONTENT_TYPE));
} }
@Test private void initialClone_Redirect(int nofRedirects, int code)
public void testInitialClone_RedirectSmall() throws Exception { throws Exception {
Repository dst = createBareRepository(); Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt)); assertFalse(dst.hasObject(A_txt));
try (Transport t = Transport.open(dst, redirectURI)) { URIish cloneFrom = redirectURI;
if (code != 301 || nofRedirects > 1) {
cloneFrom = extendPath(cloneFrom,
"/response/" + nofRedirects + "/" + code);
}
try (Transport t = Transport.open(dst, cloneFrom)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master)); t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
} }
@ -387,12 +480,15 @@ public class SmartClientSmartServerTest extends HttpTestCase {
fsck(dst, B); fsck(dst, B);
List<AccessEvent> requests = getRequests(); List<AccessEvent> requests = getRequests();
assertEquals(4, requests.size()); assertEquals(2 + nofRedirects, requests.size());
AccessEvent firstRedirect = requests.get(0); int n = 0;
assertEquals(301, firstRedirect.getStatus()); while (n < nofRedirects) {
AccessEvent redirect = requests.get(n++);
assertEquals(code, redirect.getStatus());
}
AccessEvent info = requests.get(1); AccessEvent info = requests.get(n++);
assertEquals("GET", info.getMethod()); assertEquals("GET", info.getMethod());
assertEquals(join(remoteURI, "info/refs"), info.getPath()); assertEquals(join(remoteURI, "info/refs"), info.getPath());
assertEquals(1, info.getParameters().size()); assertEquals(1, info.getParameters().size());
@ -402,10 +498,130 @@ public class SmartClientSmartServerTest extends HttpTestCase {
info.getResponseHeader(HDR_CONTENT_TYPE)); info.getResponseHeader(HDR_CONTENT_TYPE));
assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING)); assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
AccessEvent secondRedirect = requests.get(2); AccessEvent service = requests.get(n++);
assertEquals(301, secondRedirect.getStatus()); 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_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 {
FileBasedConfig userConfig = SystemReader.getInstance()
.openUserConfig(null, FS.DETECTED);
userConfig.setInt("http", null, "maxRedirects", 4);
userConfig.save();
initialClone_Redirect(4, 302);
}
@Test
public void testInitialClone_RedirectTooOften() throws Exception {
FileBasedConfig userConfig = SystemReader.getInstance()
.openUserConfig(null, FS.DETECTED);
userConfig.setInt("http", null, "maxRedirects", 3);
userConfig.save();
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(redirectURI, "/response/4/302");
String remoteUri = cloneFrom.toString();
try (Transport t = Transport.open(dst, cloneFrom)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
fail("Should have failed (too many redirects)");
} catch (TransportException e) {
String expectedMessageBegin = MessageFormat.format(
JGitText.get().redirectLimitExceeded, remoteUri, "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 {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(redirectURI, "/loop");
try (Transport t = Transport.open(dst, cloneFrom)) {
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 {
FileBasedConfig userConfig = SystemReader.getInstance()
.openUserConfig(null, FS.DETECTED);
userConfig.setString("http", null, "followRedirects", "true");
userConfig.save();
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(remoteURI, "/post");
try (Transport t = Transport.open(dst, cloneFrom)) {
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(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));
assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
AccessEvent redirect = requests.get(1);
assertEquals("POST", redirect.getMethod());
assertEquals(301, redirect.getStatus());
AccessEvent service = requests.get(3); AccessEvent service = requests.get(2);
assertEquals("POST", service.getMethod()); assertEquals("POST", service.getMethod());
assertEquals(join(remoteURI, "git-upload-pack"), service.getPath()); assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
assertEquals(0, service.getParameters().size()); assertEquals(0, service.getParameters().size());
@ -419,6 +635,39 @@ public class SmartClientSmartServerTest extends HttpTestCase {
service.getResponseHeader(HDR_CONTENT_TYPE)); service.getResponseHeader(HDR_CONTENT_TYPE));
} }
@Test
public void testInitialClone_RedirectOnPostForbidden() throws Exception {
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
URIish cloneFrom = extendPath(remoteURI, "/post");
try (Transport t = Transport.open(dst, cloneFrom)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
fail("Should have failed (redirect on POST)");
} catch (TransportException e) {
assertTrue(e.getMessage().contains("301"));
}
}
@Test
public void testInitialClone_RedirectForbidden() throws Exception {
FileBasedConfig userConfig = SystemReader.getInstance()
.openUserConfig(null, FS.DETECTED);
userConfig.setString("http", null, "followRedirects", "false");
userConfig.save();
Repository dst = createBareRepository();
assertFalse(dst.hasObject(A_txt));
try (Transport t = Transport.open(dst, redirectURI)) {
t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
fail("Should have failed (redirects forbidden)");
} catch (TransportException e) {
assertTrue(
e.getMessage().contains("http.followRedirects is false"));
}
}
@Test @Test
public void testFetch_FewLocalCommits() throws Exception { public void testFetch_FewLocalCommits() throws Exception {
// Bootstrap by doing the clone. // Bootstrap by doing the clone.
@ -619,7 +868,7 @@ public class SmartClientSmartServerTest extends HttpTestCase {
ServletContextHandler app = noRefServer.addContext("/git"); ServletContextHandler app = noRefServer.addContext("/git");
GitServlet gs = new GitServlet(); GitServlet gs = new GitServlet();
gs.setRepositoryResolver(new TestRepoResolver(repo, repoName)); gs.setRepositoryResolver(new TestRepositoryResolver(repo, repoName));
app.addServlet(new ServletHolder(gs), "/*"); app.addServlet(new ServletHolder(gs), "/*");
noRefServer.setUp(); noRefServer.setUp();
@ -822,28 +1071,4 @@ public class SmartClientSmartServerTest extends HttpTestCase {
cfg.save(); cfg.save();
} }
private final class TestRepoResolver
implements RepositoryResolver<HttpServletRequest> {
private final TestRepository<Repository> repo;
private final String repoName;
private TestRepoResolver(TestRepository<Repository> repo,
String repoName) {
this.repo = repo;
this.repoName = repoName;
}
@Override
public Repository open(HttpServletRequest req, String name)
throws RepositoryNotFoundException, ServiceNotEnabledException {
if (!name.equals(repoName))
throw new RepositoryNotFoundException(name);
Repository db = repo.getRepository();
db.incrementOpen();
return db;
}
}
} }

1
org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF

@ -20,6 +20,7 @@ Import-Package: javax.servlet;version="[2.5.0,3.2.0)",
org.eclipse.jetty.util.component;version="[9.4.5,10.0.0)", org.eclipse.jetty.util.component;version="[9.4.5,10.0.0)",
org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)", org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)", org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
org.eclipse.jetty.util.ssl;version="[9.4.5,10.0.0)",
org.eclipse.jgit.errors;version="[4.9.0,4.10.0)", org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
org.eclipse.jgit.http.server;version="[4.9.0,4.10.0)", org.eclipse.jgit.http.server;version="[4.9.0,4.10.0)",
org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)", org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",

136
org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2010, 2012 Google Inc. * Copyright (C) 2010, 2017 Google Inc.
* and other copyright owners as documented in the project's IP log. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -46,15 +46,19 @@ package org.eclipse.jgit.junit.http;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.security.AbstractLoginService; import org.eclipse.jetty.security.AbstractLoginService;
import org.eclipse.jetty.security.Authenticator; import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintMapping;
@ -65,10 +69,12 @@ import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password; import org.eclipse.jetty.util.security.Password;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.URIish;
/** /**
@ -88,6 +94,9 @@ public class AppServer {
/** Password for {@link #username} in secured access areas. */ /** Password for {@link #username} in secured access areas. */
public static final String password = "letmein"; public static final String password = "letmein";
/** SSL keystore password; must have at least 6 characters. */
private static final String keyPassword = "mykeys";
static { static {
// Install a logger that throws warning messages. // Install a logger that throws warning messages.
// //
@ -97,48 +106,141 @@ public class AppServer {
private final Server server; private final Server server;
private final HttpConfiguration config;
private final ServerConnector connector; private final ServerConnector connector;
private final HttpConfiguration secureConfig;
private final ServerConnector secureConnector;
private final ContextHandlerCollection contexts; private final ContextHandlerCollection contexts;
private final TestRequestLog log; private final TestRequestLog log;
private List<File> filesToDelete = new ArrayList<>();
public AppServer() { public AppServer() {
this(0); this(0, -1);
} }
/** /**
* @param port * @param port
* the http port number * the http port number; may be zero to allocate a port
* dynamically
* @since 4.2 * @since 4.2
*/ */
public AppServer(int port) { public AppServer(int port) {
this(port, -1);
}
/**
* @param port
* for https, may be zero to allocate a port dynamically
* @param sslPort
* for https,may be zero to allocate a port dynamically. If
* negative, the server will be set up without https support..
* @since 4.9
*/
public AppServer(int port, int sslPort) {
server = new Server(); server = new Server();
HttpConfiguration http_config = new HttpConfiguration(); config = new HttpConfiguration();
http_config.setSecureScheme("https"); config.setSecureScheme("https");
http_config.setSecurePort(8443); config.setSecurePort(0);
http_config.setOutputBufferSize(32768); config.setOutputBufferSize(32768);
connector = new ServerConnector(server, connector = new ServerConnector(server,
new HttpConnectionFactory(http_config)); new HttpConnectionFactory(config));
connector.setPort(port); connector.setPort(port);
String ip;
String hostName;
try { try {
final InetAddress me = InetAddress.getByName("localhost"); final InetAddress me = InetAddress.getByName("localhost");
connector.setHost(me.getHostAddress()); ip = me.getHostAddress();
connector.setHost(ip);
hostName = InetAddress.getLocalHost().getCanonicalHostName();
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
throw new RuntimeException("Cannot find localhost", e); throw new RuntimeException("Cannot find localhost", e);
} }
if (sslPort >= 0) {
SslContextFactory sslContextFactory = createTestSslContextFactory(
hostName);
secureConfig = new HttpConfiguration(config);
secureConnector = new ServerConnector(server,
new SslConnectionFactory(sslContextFactory,
HttpVersion.HTTP_1_1.asString()),
new HttpConnectionFactory(secureConfig));
secureConnector.setPort(sslPort);
secureConnector.setHost(ip);
} else {
secureConfig = null;
secureConnector = null;
}
contexts = new ContextHandlerCollection(); contexts = new ContextHandlerCollection();
log = new TestRequestLog(); log = new TestRequestLog();
log.setHandler(contexts); log.setHandler(contexts);
server.setConnectors(new Connector[] { connector }); if (secureConnector == null) {
server.setConnectors(new Connector[] { connector });
} else {
server.setConnectors(
new Connector[] { connector, secureConnector });
}
server.setHandler(log); server.setHandler(log);
} }
private SslContextFactory createTestSslContextFactory(String hostName) {
SslContextFactory factory = new SslContextFactory(true);
String dName = "CN=,OU=,O=,ST=,L=,C=";
try {
File tmpDir = Files.createTempDirectory("jks").toFile();
tmpDir.deleteOnExit();
makePrivate(tmpDir);
File keyStore = new File(tmpDir, "keystore.jks");
Runtime.getRuntime().exec(
new String[] {
"keytool", //
"-keystore", keyStore.getAbsolutePath(), //
"-storepass", keyPassword,
"-alias", hostName, //
"-genkeypair", //
"-keyalg", "RSA", //
"-keypass", keyPassword, //
"-dname", dName, //
"-validity", "2" //
}).waitFor();
keyStore.deleteOnExit();
makePrivate(keyStore);
filesToDelete.add(keyStore);
filesToDelete.add(tmpDir);
factory.setKeyStorePath(keyStore.getAbsolutePath());
factory.setKeyStorePassword(keyPassword);
factory.setKeyManagerPassword(keyPassword);
factory.setTrustStorePath(keyStore.getAbsolutePath());
factory.setTrustStorePassword(keyPassword);
} catch (InterruptedException | IOException e) {
throw new RuntimeException("Cannot create ssl key/certificate", e);
}
return factory;
}
private void makePrivate(File file) {
file.setReadable(false);
file.setWritable(false);
file.setExecutable(false);
file.setReadable(true, true);
file.setWritable(true, true);
if (file.isDirectory()) {
file.setExecutable(true, true);
}
}
/** /**
* Create a new servlet context within the server. * Create a new servlet context within the server.
* <p> * <p>
@ -231,6 +333,10 @@ public class AppServer {
RecordingLogger.clear(); RecordingLogger.clear();
log.clear(); log.clear();
server.start(); server.start();
config.setSecurePort(getSecurePort());
if (secureConfig != null) {
secureConfig.setSecurePort(getSecurePort());
}
} }
/** /**
@ -243,6 +349,10 @@ public class AppServer {
RecordingLogger.clear(); RecordingLogger.clear();
log.clear(); log.clear();
server.stop(); server.stop();
for (File f : filesToDelete) {
f.delete();
}
filesToDelete.clear();
} }
/** /**
@ -272,6 +382,12 @@ public class AppServer {
return connector.getLocalPort(); return connector.getLocalPort();
} }
/** @return the HTTPS port or -1 if not configured. */
public int getSecurePort() {
assertAlreadySetUp();
return secureConnector != null ? secureConnector.getLocalPort() : -1;
}
/** @return all requests since the server was started. */ /** @return all requests since the server was started. */
public List<AccessEvent> getRequests() { public List<AccessEvent> getRequests() {
return new ArrayList<>(log.getEvents()); return new ArrayList<>(log.getEvents());

51
org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2009-2010, Google Inc. * Copyright (C) 2009-2017, Google Inc.
* and other copyright owners as documented in the project's IP log. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -77,7 +77,7 @@ public abstract class HttpTestCase extends LocalDiskRepositoryTestCase {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
super.setUp(); super.setUp();
server = new AppServer(); server = createServer();
} }
@Override @Override
@ -86,6 +86,20 @@ public abstract class HttpTestCase extends LocalDiskRepositoryTestCase {
super.tearDown(); super.tearDown();
} }
/**
* Creates the {@linkAppServer}.This default implementation creates a server
* without SSLsupport listening for HTTP connections on a dynamically chosen
* port, which can be gotten once the server has been started via its
* {@link AppServer#getPort()} method. Subclasses may override if they need
* a more specialized server.
*
* @return the {@link AppServer}.
* @since 4.9
*/
protected AppServer createServer() {
return new AppServer();
}
protected TestRepository<Repository> createTestRepository() protected TestRepository<Repository> createTestRepository()
throws IOException { throws IOException {
return new TestRepository<>(createBareRepository()); return new TestRepository<>(createBareRepository());
@ -165,4 +179,37 @@ public abstract class HttpTestCase extends LocalDiskRepositoryTestCase {
dir += "/"; dir += "/";
return dir + path; return dir + path;
} }
protected static String rewriteUrl(String url, String newProtocol,
int newPort) {
String newUrl = url;
if (newProtocol != null && !newProtocol.isEmpty()) {
int schemeEnd = newUrl.indexOf("://");
if (schemeEnd >= 0) {
newUrl = newProtocol + newUrl.substring(schemeEnd);
}
}
if (newPort > 0) {
newUrl = newUrl.replaceFirst(":\\d+/", ":" + newPort + "/");
} else {
// Remove the port, if any
newUrl = newUrl.replaceFirst(":\\d+/", "/");
}
return newUrl;
}
protected static URIish extendPath(URIish uri, String pathComponents)
throws URISyntaxException {
String raw = uri.toString();
String newComponents = pathComponents;
if (!newComponents.startsWith("/")) {
newComponents = '/' + newComponents;
}
if (!newComponents.endsWith("/")) {
newComponents += '/';
}
int i = raw.lastIndexOf('/');
raw = raw.substring(0, i) + newComponents + raw.substring(i + 1);
return new URIish(raw);
}
} }

20
org.eclipse.jgit/.settings/.api_filters

@ -52,4 +52,24 @@
</message_arguments> </message_arguments>
</filter> </filter>
</resource> </resource>
<resource path="src/org/eclipse/jgit/transport/http/HttpConnection.java" type="org.eclipse.jgit.transport.http.HttpConnection">
<filter id="403767336">
<message_arguments>
<message_argument value="org.eclipse.jgit.transport.http.HttpConnection"/>
<message_argument value="HTTP_11_MOVED_TEMP"/>
</message_arguments>
</filter>
<filter id="403767336">
<message_arguments>
<message_argument value="org.eclipse.jgit.transport.http.HttpConnection"/>
<message_argument value="HTTP_MOVED_TEMP"/>
</message_arguments>
</filter>
<filter id="403767336">
<message_arguments>
<message_argument value="org.eclipse.jgit.transport.http.HttpConnection"/>
<message_argument value="HTTP_SEE_OTHER"/>
</message_arguments>
</filter>
</resource>
</component> </component>

7
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties

@ -365,12 +365,14 @@ 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}
invalidReflogRevision=Invalid reflog revision: {0} invalidReflogRevision=Invalid reflog revision: {0}
invalidRefName=Invalid ref name: {0} invalidRefName=Invalid ref name: {0}
invalidRemote=Invalid remote: {0} invalidRemote=Invalid remote: {0}
invalidRepositoryStateNoHead=Invalid repository --- cannot read HEAD invalidRepositoryStateNoHead=Invalid repository --- cannot read HEAD
invalidShallowObject=invalid shallow object {0}, expected commit invalidShallowObject=invalid shallow object {0}, expected commit
invalidStageForPath=Invalid stage {0} for path {1} invalidStageForPath=Invalid stage {0} for path {1}
invalidSystemProperty=Invalid system property ''{0}'': ''{1}''; using default value {2}
invalidTagOption=Invalid tag option: {0} invalidTagOption=Invalid tag option: {0}
invalidTimeout=Invalid timeout: {0} invalidTimeout=Invalid timeout: {0}
invalidTimeUnitValue2=Invalid time unit value: {0}.{1}={2} invalidTimeUnitValue2=Invalid time unit value: {0}.{1}={2}
@ -528,6 +530,11 @@ receivePackObjectTooLarge2=Object too large ({0} bytes), rejecting the pack. Max
receivePackInvalidLimit=Illegal limit parameter value {0} receivePackInvalidLimit=Illegal limit parameter value {0}
receivePackTooLarge=Pack exceeds the limit of {0} bytes, rejecting the pack receivePackTooLarge=Pack exceeds the limit of {0} bytes, rejecting the pack
receivingObjects=Receiving objects receivingObjects=Receiving objects
redirectBlocked=URI ''{0}'' redirection blocked: redirect {1} -> {2} not allowed
redirectHttp=URI ''{0}'': following HTTP redirect #{1} {2} -> {3}
redirectLimitExceeded=URI ''{0}'' redirected more than {1} times; aborted at {2} -> {3}
redirectLocationMissing=Invalid redirect of ''{0}'': no redirect location for {1}
redirectsOff=Cannot redirect ''{0}'': http.followRedirects is false (HTTP status {1})
refAlreadyExists=already exists refAlreadyExists=already exists
refAlreadyExists1=Ref {0} already exists refAlreadyExists1=Ref {0} already exists
reflogEntryNotFound=Entry {0} not found in reflog for ''{1}'' reflogEntryNotFound=Entry {0} not found in reflog for ''{1}''

7
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java

@ -424,11 +424,13 @@ public class JGitText extends TranslationBundle {
/***/ public String invalidPathPeriodAtEndWindows; /***/ public String invalidPathPeriodAtEndWindows;
/***/ public String invalidPathSpaceAtEndWindows; /***/ public String invalidPathSpaceAtEndWindows;
/***/ public String invalidPathReservedOnWindows; /***/ public String invalidPathReservedOnWindows;
/***/ public String invalidRedirectLocation;
/***/ public String invalidReflogRevision; /***/ public String invalidReflogRevision;
/***/ public String invalidRefName; /***/ public String invalidRefName;
/***/ public String invalidRemote; /***/ public String invalidRemote;
/***/ public String invalidShallowObject; /***/ public String invalidShallowObject;
/***/ public String invalidStageForPath; /***/ public String invalidStageForPath;
/***/ public String invalidSystemProperty;
/***/ public String invalidTagOption; /***/ public String invalidTagOption;
/***/ public String invalidTimeout; /***/ public String invalidTimeout;
/***/ public String invalidTimeUnitValue2; /***/ public String invalidTimeUnitValue2;
@ -587,6 +589,11 @@ public class JGitText extends TranslationBundle {
/***/ public String receivePackInvalidLimit; /***/ public String receivePackInvalidLimit;
/***/ public String receivePackTooLarge; /***/ public String receivePackTooLarge;
/***/ public String receivingObjects; /***/ public String receivingObjects;
/***/ public String redirectBlocked;
/***/ public String redirectHttp;
/***/ public String redirectLimitExceeded;
/***/ public String redirectLocationMissing;
/***/ public String redirectsOff;
/***/ public String refAlreadyExists; /***/ public String refAlreadyExists;
/***/ public String refAlreadyExists1; /***/ public String refAlreadyExists1;
/***/ public String reflogEntryNotFound; /***/ public String reflogEntryNotFound;

304
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java

@ -2,6 +2,7 @@
* Copyright (C) 2008-2010, Google Inc. * Copyright (C) 2008-2010, Google Inc.
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2013, Matthias Sohn <matthias.sohn@sap.com> * Copyright (C) 2013, Matthias Sohn <matthias.sohn@sap.com>
* Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch>
* and other copyright owners as documented in the project's IP log. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -69,6 +70,7 @@ import java.io.OutputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.Proxy; import java.net.Proxy;
import java.net.ProxySelector; import java.net.ProxySelector;
import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -78,9 +80,11 @@ import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.function.Supplier;
import java.util.zip.GZIPInputStream; import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream; import java.util.zip.GZIPOutputStream;
@ -103,9 +107,12 @@ import org.eclipse.jgit.transport.http.HttpConnection;
import org.eclipse.jgit.util.HttpSupport; import org.eclipse.jgit.util.HttpSupport;
import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.io.DisabledOutputStream; import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.eclipse.jgit.util.io.UnionInputStream; import org.eclipse.jgit.util.io.UnionInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Transport over HTTP and FTP protocols. * Transport over HTTP and FTP protocols.
@ -126,10 +133,37 @@ import org.eclipse.jgit.util.io.UnionInputStream;
public class TransportHttp extends HttpTransport implements WalkTransport, public class TransportHttp extends HttpTransport implements WalkTransport,
PackTransport { PackTransport {
private static final Logger LOG = LoggerFactory
.getLogger(TransportHttp.class);
private static final String SVC_UPLOAD_PACK = "git-upload-pack"; //$NON-NLS-1$ private static final String SVC_UPLOAD_PACK = "git-upload-pack"; //$NON-NLS-1$
private static final String SVC_RECEIVE_PACK = "git-receive-pack"; //$NON-NLS-1$ private static final String SVC_RECEIVE_PACK = "git-receive-pack"; //$NON-NLS-1$
private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$
private static final int DEFAULT_MAX_REDIRECTS = 5;
private static final int MAX_REDIRECTS = (new Supplier<Integer>() {
@Override
public Integer get() {
String rawValue = SystemReader.getInstance()
.getProperty(MAX_REDIRECT_SYSTEM_PROPERTY);
Integer value = Integer.valueOf(DEFAULT_MAX_REDIRECTS);
if (rawValue != null) {
try {
value = Integer.valueOf(Integer.parseUnsignedInt(rawValue));
} catch (NumberFormatException e) {
LOG.warn(MessageFormat.format(
JGitText.get().invalidSystemProperty,
MAX_REDIRECT_SYSTEM_PROPERTY, rawValue, value));
}
}
return value;
}
}).get().intValue();
/** /**
* Accept-Encoding header in the HTTP request * Accept-Encoding header in the HTTP request
* (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html). * (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html).
@ -230,14 +264,58 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
} }
}; };
/**
* Config values for http.followRedirect
*/
private static enum HttpRedirectMode implements Config.ConfigEnum {
/** Always follow redirects (up to the http.maxRedirects limit). */
TRUE("true"), //$NON-NLS-1$
/**
* Only follow redirects on the initial GET request. This is the
* default.
*/
INITIAL("initial"), //$NON-NLS-1$
/** Never follow redirects. */
FALSE("false"); //$NON-NLS-1$
private final String configValue;
private HttpRedirectMode(String configValue) {
this.configValue = configValue;
}
@Override
public String toConfigValue() {
return configValue;
}
@Override
public boolean matchConfigValue(String s) {
return configValue.equals(s);
}
}
private static class HttpConfig { private static class HttpConfig {
final int postBuffer; final int postBuffer;
final boolean sslVerify; final boolean sslVerify;
final HttpRedirectMode followRedirects;
final int maxRedirects;
HttpConfig(final Config rc) { HttpConfig(final Config rc) {
postBuffer = rc.getInt("http", "postbuffer", 1 * 1024 * 1024); //$NON-NLS-1$ //$NON-NLS-2$ postBuffer = rc.getInt("http", "postbuffer", 1 * 1024 * 1024); //$NON-NLS-1$ //$NON-NLS-2$
sslVerify = rc.getBoolean("http", "sslVerify", true); //$NON-NLS-1$ //$NON-NLS-2$ sslVerify = rc.getBoolean("http", "sslVerify", true); //$NON-NLS-1$ //$NON-NLS-2$
followRedirects = rc.getEnum(HttpRedirectMode.values(), "http", //$NON-NLS-1$
null, "followRedirects", HttpRedirectMode.INITIAL); //$NON-NLS-1$
int redirectLimit = rc.getInt("http", "maxRedirects", //$NON-NLS-1$ //$NON-NLS-2$
MAX_REDIRECTS);
if (redirectLimit < 0) {
redirectLimit = MAX_REDIRECTS;
}
maxRedirects = redirectLimit;
} }
HttpConfig() { HttpConfig() {
@ -245,9 +323,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
} }
} }
final URL baseUrl; private URL baseUrl;
private final URL objectsUrl; private URL objectsUrl;
final HttpConfig http; final HttpConfig http;
@ -262,17 +340,31 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
TransportHttp(final Repository local, final URIish uri) TransportHttp(final Repository local, final URIish uri)
throws NotSupportedException { throws NotSupportedException {
super(local, uri); super(local, uri);
setURI(uri);
http = local.getConfig().get(HttpConfig::new);
proxySelector = ProxySelector.getDefault();
}
private URL toURL(URIish urish) throws MalformedURLException {
String uriString = urish.toString();
if (!uriString.endsWith("/")) { //$NON-NLS-1$
uriString += '/';
}
return new URL(uriString);
}
/**
* @param uri
* @throws NotSupportedException
* @since 4.9
*/
protected void setURI(final URIish uri) throws NotSupportedException {
try { try {
String uriString = uri.toString(); baseUrl = toURL(uri);
if (!uriString.endsWith("/")) //$NON-NLS-1$
uriString += "/"; //$NON-NLS-1$
baseUrl = new URL(uriString);
objectsUrl = new URL(baseUrl, "objects/"); //$NON-NLS-1$ objectsUrl = new URL(baseUrl, "objects/"); //$NON-NLS-1$
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
throw new NotSupportedException(MessageFormat.format(JGitText.get().invalidURL, uri), e); throw new NotSupportedException(MessageFormat.format(JGitText.get().invalidURL, uri), e);
} }
http = local.getConfig().get(HttpConfig::new);
proxySelector = ProxySelector.getDefault();
} }
/** /**
@ -283,15 +375,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
*/ */
TransportHttp(final URIish uri) throws NotSupportedException { TransportHttp(final URIish uri) throws NotSupportedException {
super(uri); super(uri);
try { setURI(uri);
String uriString = uri.toString();
if (!uriString.endsWith("/")) //$NON-NLS-1$
uriString += "/"; //$NON-NLS-1$
baseUrl = new URL(uriString);
objectsUrl = new URL(baseUrl, "objects/"); //$NON-NLS-1$
} catch (MalformedURLException e) {
throw new NotSupportedException(MessageFormat.format(JGitText.get().invalidURL, uri), e);
}
http = new HttpConfig(); http = new HttpConfig();
proxySelector = ProxySelector.getDefault(); proxySelector = ProxySelector.getDefault();
} }
@ -461,28 +545,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
private HttpConnection connect(final String service) private HttpConnection connect(final String service)
throws TransportException, NotSupportedException { throws TransportException, NotSupportedException {
final URL u; URL u = getServiceURL(service);
try {
final StringBuilder b = new StringBuilder();
b.append(baseUrl);
if (b.charAt(b.length() - 1) != '/')
b.append('/');
b.append(Constants.INFO_REFS);
if (useSmartHttp) {
b.append(b.indexOf("?") < 0 ? '?' : '&'); //$NON-NLS-1$
b.append("service="); //$NON-NLS-1$
b.append(service);
}
u = new URL(b.toString());
} catch (MalformedURLException e) {
throw new NotSupportedException(MessageFormat.format(JGitText.get().invalidURL, uri), e);
}
int authAttempts = 1; int authAttempts = 1;
int redirects = 0;
Collection<Type> ignoreTypes = null; Collection<Type> ignoreTypes = null;
for (;;) { for (;;) {
try { try {
@ -532,6 +597,24 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
throw new TransportException(uri, MessageFormat.format( throw new TransportException(uri, MessageFormat.format(
JGitText.get().serviceNotPermitted, service)); JGitText.get().serviceNotPermitted, service));
case HttpConnection.HTTP_MOVED_PERM:
case HttpConnection.HTTP_MOVED_TEMP:
case HttpConnection.HTTP_SEE_OTHER:
case HttpConnection.HTTP_11_MOVED_TEMP:
// SEE_OTHER should actually never be sent by a git server,
// and in general should occur only on POST requests. But it
// doesn't hurt to accept it here as a redirect.
if (http.followRedirects == HttpRedirectMode.FALSE) {
throw new TransportException(MessageFormat.format(
JGitText.get().redirectsOff, uri,
Integer.valueOf(status)));
}
URIish newUri = redirect(conn.getHeaderField(HDR_LOCATION),
Constants.INFO_REFS, redirects++);
setURI(newUri);
u = getServiceURL(service);
authAttempts = 1;
break;
default: default:
String err = status + " " + conn.getResponseMessage(); //$NON-NLS-1$ String err = status + " " + conn.getResponseMessage(); //$NON-NLS-1$
throw new TransportException(uri, err); throw new TransportException(uri, err);
@ -560,6 +643,86 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
} }
} }
private URIish redirect(String location, String checkFor, int redirects)
throws TransportException {
if (location == null || location.isEmpty()) {
throw new TransportException(MessageFormat.format(
JGitText.get().redirectLocationMissing, uri, baseUrl));
}
if (redirects >= http.maxRedirects) {
throw new TransportException(MessageFormat.format(
JGitText.get().redirectLimitExceeded, uri,
Integer.valueOf(http.maxRedirects), baseUrl, location));
}
try {
if (!isValidRedirect(baseUrl, location, checkFor)) {
throw new TransportException(
MessageFormat.format(JGitText.get().redirectBlocked,
uri, baseUrl, location));
}
location = location.substring(0, location.indexOf(checkFor));
URIish result = new URIish(location);
if (LOG.isInfoEnabled()) {
LOG.info(MessageFormat.format(JGitText.get().redirectHttp, uri,
Integer.valueOf(redirects), baseUrl, result));
}
return result;
} catch (URISyntaxException e) {
throw new TransportException(MessageFormat.format(
JGitText.get().invalidRedirectLocation,
uri, baseUrl, location), e);
}
}
private boolean isValidRedirect(URL current, String next, String checkFor) {
// Protocols must be the same, or current is "http" and next "https". We
// do not follow redirects from https back to http.
String oldProtocol = current.getProtocol().toLowerCase(Locale.ROOT);
int schemeEnd = next.indexOf("://"); //$NON-NLS-1$
if (schemeEnd < 0) {
return false;
}
String newProtocol = next.substring(0, schemeEnd)
.toLowerCase(Locale.ROOT);
if (!oldProtocol.equals(newProtocol)) {
if (!"https".equals(newProtocol)) { //$NON-NLS-1$
return false;
}
}
// git allows only rewriting the root, i.e., everything before INFO_REFS
// or the service name
if (next.indexOf(checkFor) < 0) {
return false;
}
// Basically we should test here that whatever follows INFO_REFS is
// unchanged. But since we re-construct the query part
// anyway, it doesn't matter.
return true;
}
private URL getServiceURL(final String service)
throws NotSupportedException {
try {
final StringBuilder b = new StringBuilder();
b.append(baseUrl);
if (b.charAt(b.length() - 1) != '/') {
b.append('/');
}
b.append(Constants.INFO_REFS);
if (useSmartHttp) {
b.append(b.indexOf("?") < 0 ? '?' : '&'); //$NON-NLS-1$
b.append("service="); //$NON-NLS-1$
b.append(service);
}
return new URL(b.toString());
} catch (MalformedURLException e) {
throw new NotSupportedException(MessageFormat.format(JGitText.get().invalidURL, uri), e);
}
}
/** /**
* Open an HTTP connection, setting the accept-encoding request header to gzip. * Open an HTTP connection, setting the accept-encoding request header to gzip.
* *
@ -598,6 +761,10 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
HttpSupport.disableSslVerify(conn); HttpSupport.disableSslVerify(conn);
} }
// We must do our own redirect handling to implement git rules and to
// handle http->https redirects
conn.setInstanceFollowRedirects(false);
conn.setRequestMethod(method); conn.setRequestMethod(method);
conn.setUseCaches(false); conn.setUseCaches(false);
if (acceptEncoding == AcceptEncoding.GZIP) { if (acceptEncoding == AcceptEncoding.GZIP) {
@ -906,13 +1073,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
} }
void openStream() throws IOException { void openStream() throws IOException {
openStream(null); conn = httpOpen(METHOD_POST, new URL(baseUrl, serviceName),
}
void openStream(final String redirectUrl) throws IOException {
conn = httpOpen(
METHOD_POST,
redirectUrl == null ? new URL(baseUrl, serviceName) : new URL(redirectUrl),
AcceptEncoding.GZIP); AcceptEncoding.GZIP);
conn.setInstanceFollowRedirects(false); conn.setInstanceFollowRedirects(false);
conn.setDoOutput(true); conn.setDoOutput(true);
@ -921,10 +1082,6 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
} }
void sendRequest() throws IOException { void sendRequest() throws IOException {
sendRequest(null);
}
void sendRequest(final String redirectUrl) throws IOException {
// Try to compress the content, but only if that is smaller. // Try to compress the content, but only if that is smaller.
TemporaryBuffer buf = new TemporaryBuffer.Heap(http.postBuffer); TemporaryBuffer buf = new TemporaryBuffer.Heap(http.postBuffer);
try { try {
@ -939,21 +1096,42 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
buf = out; buf = out;
} }
openStream(redirectUrl); int redirects = 0;
if (buf != out) for (;;) {
conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP); openStream();
conn.setFixedLengthStreamingMode((int) buf.length()); if (buf != out) {
final OutputStream httpOut = conn.getOutputStream(); conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP);
try { }
buf.writeTo(httpOut, null); conn.setFixedLengthStreamingMode((int) buf.length());
} finally { try (OutputStream httpOut = conn.getOutputStream()) {
httpOut.close(); buf.writeTo(httpOut, null);
} }
final int status = HttpSupport.response(conn); if (http.followRedirects == HttpRedirectMode.TRUE) {
if (status == HttpConnection.HTTP_MOVED_PERM) { final int status = HttpSupport.response(conn);
String locationHeader = HttpSupport.responseHeader(conn, HDR_LOCATION); switch (status) {
sendRequest(locationHeader); case HttpConnection.HTTP_MOVED_PERM:
case HttpConnection.HTTP_MOVED_TEMP:
case HttpConnection.HTTP_11_MOVED_TEMP:
// SEE_OTHER after a POST doesn't make sense for a git
// server, so we don't handle it here and thus we'll
// report an error in openResponse() later on.
URIish newUri = redirect(
conn.getHeaderField(HDR_LOCATION),
'/' + serviceName, redirects++);
try {
baseUrl = toURL(newUri);
} catch (MalformedURLException e) {
throw new TransportException(MessageFormat.format(
JGitText.get().invalidRedirectLocation,
uri, baseUrl, newUri), e);
}
continue;
default:
break;
}
}
break;
} }
} }

24
org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2013 Christian Halstrick <christian.halstrick@sap.com> * Copyright (C) 2013, 2017 Christian Halstrick <christian.halstrick@sap.com>
* and other copyright owners as documented in the project's IP log. * and other copyright owners as documented in the project's IP log.
* *
* This program and the accompanying materials are made available * This program and the accompanying materials are made available
@ -78,6 +78,26 @@ public interface HttpConnection {
*/ */
public static final int HTTP_MOVED_PERM = java.net.HttpURLConnection.HTTP_MOVED_PERM; public static final int HTTP_MOVED_PERM = java.net.HttpURLConnection.HTTP_MOVED_PERM;
/**
* @see HttpURLConnection#HTTP_MOVED_TEMP
* @since 4.9
*/
public static final int HTTP_MOVED_TEMP = java.net.HttpURLConnection.HTTP_MOVED_TEMP;
/**
* @see HttpURLConnection#HTTP_SEE_OTHER
* @since 4.9
*/
public static final int HTTP_SEE_OTHER = java.net.HttpURLConnection.HTTP_SEE_OTHER;
/**
* HTTP 1.1 additional MOVED_TEMP status code; value = 307.
*
* @see #HTTP_MOVED_TEMP
* @since 4.9
*/
public static final int HTTP_11_MOVED_TEMP = 307;
/** /**
* @see HttpURLConnection#HTTP_NOT_FOUND * @see HttpURLConnection#HTTP_NOT_FOUND
*/ */
@ -253,7 +273,7 @@ public interface HttpConnection {
/** /**
* Configure the connection so that it can be used for https communication. * Configure the connection so that it can be used for https communication.
* *
* @param km * @param km
* the keymanager managing the key material used to authenticate * the keymanager managing the key material used to authenticate
* the local SSLSocket to its peer * the local SSLSocket to its peer

Loading…
Cancel
Save