filesToDelete = new ArrayList<>();
+
public AppServer() {
- this(0);
+ this(0, -1);
}
/**
* @param port
- * the http port number
+ * the http port number; may be zero to allocate a port
+ * dynamically
* @since 4.2
*/
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();
- HttpConfiguration http_config = new HttpConfiguration();
- http_config.setSecureScheme("https");
- http_config.setSecurePort(8443);
- http_config.setOutputBufferSize(32768);
+ config = new HttpConfiguration();
+ config.setSecureScheme("https");
+ config.setSecurePort(0);
+ config.setOutputBufferSize(32768);
connector = new ServerConnector(server,
- new HttpConnectionFactory(http_config));
+ new HttpConnectionFactory(config));
connector.setPort(port);
+ String ip;
+ String hostName;
try {
final InetAddress me = InetAddress.getByName("localhost");
- connector.setHost(me.getHostAddress());
+ ip = me.getHostAddress();
+ connector.setHost(ip);
+ hostName = InetAddress.getLocalHost().getCanonicalHostName();
} catch (UnknownHostException 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();
log = new TestRequestLog();
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);
}
+ 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.
*
@@ -231,6 +333,10 @@ public void setUp() throws Exception {
RecordingLogger.clear();
log.clear();
server.start();
+ config.setSecurePort(getSecurePort());
+ if (secureConfig != null) {
+ secureConfig.setSecurePort(getSecurePort());
+ }
}
/**
@@ -243,6 +349,10 @@ public void tearDown() throws Exception {
RecordingLogger.clear();
log.clear();
server.stop();
+ for (File f : filesToDelete) {
+ f.delete();
+ }
+ filesToDelete.clear();
}
/**
@@ -272,6 +382,12 @@ public int getPort() {
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. */
public List getRequests() {
return new ArrayList<>(log.getEvents());
diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java
index 1b94e02fa..eabb0f225 100644
--- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/HttpTestCase.java
+++ b/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.
*
* This program and the accompanying materials are made available
@@ -77,7 +77,7 @@ public abstract class HttpTestCase extends LocalDiskRepositoryTestCase {
@Override
public void setUp() throws Exception {
super.setUp();
- server = new AppServer();
+ server = createServer();
}
@Override
@@ -86,6 +86,20 @@ public void tearDown() throws Exception {
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 createTestRepository()
throws IOException {
return new TestRepository<>(createBareRepository());
@@ -165,4 +179,37 @@ public static String join(URIish base, String path) {
dir += "/";
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);
+ }
}
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 83da4c5e3..dbd754708 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -52,4 +52,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index fc29f481d..22e60cbb5 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/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}
invalidPathSpaceAtEndWindows=Invalid path (space at end is ignored by Windows): {0}
invalidPathReservedOnWindows=Invalid path (''{0}'' is reserved on Windows): {1}
+invalidRedirectLocation=Redirect or URI ''{0}'': invalid redirect location {1} -> {2}
invalidReflogRevision=Invalid reflog revision: {0}
invalidRefName=Invalid ref name: {0}
invalidRemote=Invalid remote: {0}
invalidRepositoryStateNoHead=Invalid repository --- cannot read HEAD
invalidShallowObject=invalid shallow object {0}, expected commit
invalidStageForPath=Invalid stage {0} for path {1}
+invalidSystemProperty=Invalid system property ''{0}'': ''{1}''; using default value {2}
invalidTagOption=Invalid tag option: {0}
invalidTimeout=Invalid timeout: {0}
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}
receivePackTooLarge=Pack exceeds the limit of {0} bytes, rejecting the pack
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
refAlreadyExists1=Ref {0} already exists
reflogEntryNotFound=Entry {0} not found in reflog for ''{1}''
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index a68f839c5..610b99c14 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -424,11 +424,13 @@ public static JGitText get() {
/***/ public String invalidPathPeriodAtEndWindows;
/***/ public String invalidPathSpaceAtEndWindows;
/***/ public String invalidPathReservedOnWindows;
+ /***/ public String invalidRedirectLocation;
/***/ public String invalidReflogRevision;
/***/ public String invalidRefName;
/***/ public String invalidRemote;
/***/ public String invalidShallowObject;
/***/ public String invalidStageForPath;
+ /***/ public String invalidSystemProperty;
/***/ public String invalidTagOption;
/***/ public String invalidTimeout;
/***/ public String invalidTimeUnitValue2;
@@ -587,6 +589,11 @@ public static JGitText get() {
/***/ public String receivePackInvalidLimit;
/***/ public String receivePackTooLarge;
/***/ public String receivingObjects;
+ /***/ public String redirectBlocked;
+ /***/ public String redirectHttp;
+ /***/ public String redirectLimitExceeded;
+ /***/ public String redirectLocationMissing;
+ /***/ public String redirectsOff;
/***/ public String refAlreadyExists;
/***/ public String refAlreadyExists1;
/***/ public String reflogEntryNotFound;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
index 1bdecdb18..327dc39c7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
@@ -2,6 +2,7 @@
* Copyright (C) 2008-2010, Google Inc.
* Copyright (C) 2008, Shawn O. Pearce
* Copyright (C) 2013, Matthias Sohn
+ * Copyright (C) 2017, Thomas Wolf
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -69,6 +70,7 @@
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.ProxySelector;
+import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -78,9 +80,11 @@
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
+import java.util.function.Supplier;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@@ -103,9 +107,12 @@
import org.eclipse.jgit.util.HttpSupport;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.eclipse.jgit.util.io.UnionInputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Transport over HTTP and FTP protocols.
@@ -126,10 +133,37 @@
public class TransportHttp extends HttpTransport implements WalkTransport,
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_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() {
+
+ @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
* (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html).
@@ -230,14 +264,58 @@ public Transport open(URIish uri, Repository local, String remoteName)
}
};
+ /**
+ * 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 {
final int postBuffer;
final boolean sslVerify;
+ final HttpRedirectMode followRedirects;
+
+ final int maxRedirects;
+
HttpConfig(final Config rc) {
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$
+ 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() {
@@ -245,9 +323,9 @@ private static class HttpConfig {
}
}
- final URL baseUrl;
+ private URL baseUrl;
- private final URL objectsUrl;
+ private URL objectsUrl;
final HttpConfig http;
@@ -262,17 +340,31 @@ private static class HttpConfig {
TransportHttp(final Repository local, final URIish uri)
throws NotSupportedException {
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 {
- String uriString = uri.toString();
- if (!uriString.endsWith("/")) //$NON-NLS-1$
- uriString += "/"; //$NON-NLS-1$
- baseUrl = new URL(uriString);
+ baseUrl = toURL(uri);
objectsUrl = new URL(baseUrl, "objects/"); //$NON-NLS-1$
} catch (MalformedURLException e) {
throw new NotSupportedException(MessageFormat.format(JGitText.get().invalidURL, uri), e);
}
- http = local.getConfig().get(HttpConfig::new);
- proxySelector = ProxySelector.getDefault();
}
/**
@@ -283,15 +375,7 @@ private static class HttpConfig {
*/
TransportHttp(final URIish uri) throws NotSupportedException {
super(uri);
- try {
- 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);
- }
+ setURI(uri);
http = new HttpConfig();
proxySelector = ProxySelector.getDefault();
}
@@ -461,28 +545,9 @@ public void setAdditionalHeaders(Map headers) {
private HttpConnection connect(final String service)
throws TransportException, NotSupportedException {
- final URL u;
- 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);
- }
-
-
+ URL u = getServiceURL(service);
int authAttempts = 1;
+ int redirects = 0;
Collection ignoreTypes = null;
for (;;) {
try {
@@ -532,6 +597,24 @@ private HttpConnection connect(final String service)
throw new TransportException(uri, MessageFormat.format(
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:
String err = status + " " + conn.getResponseMessage(); //$NON-NLS-1$
throw new TransportException(uri, err);
@@ -560,6 +643,86 @@ private HttpConnection connect(final String service)
}
}
+ 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.
*
@@ -598,6 +761,10 @@ protected HttpConnection httpOpen(String method, URL u,
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.setUseCaches(false);
if (acceptEncoding == AcceptEncoding.GZIP) {
@@ -906,13 +1073,7 @@ abstract class Service {
}
void openStream() throws IOException {
- openStream(null);
- }
-
- void openStream(final String redirectUrl) throws IOException {
- conn = httpOpen(
- METHOD_POST,
- redirectUrl == null ? new URL(baseUrl, serviceName) : new URL(redirectUrl),
+ conn = httpOpen(METHOD_POST, new URL(baseUrl, serviceName),
AcceptEncoding.GZIP);
conn.setInstanceFollowRedirects(false);
conn.setDoOutput(true);
@@ -921,10 +1082,6 @@ void openStream(final String redirectUrl) 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.
TemporaryBuffer buf = new TemporaryBuffer.Heap(http.postBuffer);
try {
@@ -939,21 +1096,42 @@ void sendRequest(final String redirectUrl) throws IOException {
buf = out;
}
- openStream(redirectUrl);
- if (buf != out)
- conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP);
- conn.setFixedLengthStreamingMode((int) buf.length());
- final OutputStream httpOut = conn.getOutputStream();
- try {
- buf.writeTo(httpOut, null);
- } finally {
- httpOut.close();
- }
+ int redirects = 0;
+ for (;;) {
+ openStream();
+ if (buf != out) {
+ conn.setRequestProperty(HDR_CONTENT_ENCODING, ENCODING_GZIP);
+ }
+ conn.setFixedLengthStreamingMode((int) buf.length());
+ try (OutputStream httpOut = conn.getOutputStream()) {
+ buf.writeTo(httpOut, null);
+ }
- final int status = HttpSupport.response(conn);
- if (status == HttpConnection.HTTP_MOVED_PERM) {
- String locationHeader = HttpSupport.responseHeader(conn, HDR_LOCATION);
- sendRequest(locationHeader);
+ if (http.followRedirects == HttpRedirectMode.TRUE) {
+ final int status = HttpSupport.response(conn);
+ switch (status) {
+ case HttpConnection.HTTP_MOVED_PERM:
+ case HttpConnection.HTTP_MOVED_TEMP:
+ case HttpConnection.HTTP_11_MOVED_TEMP:
+ // SEE_OTHER after a POST doesn't make sense for a git
+ // server, so we don't handle it here and thus we'll
+ // report an error in openResponse() later on.
+ URIish newUri = redirect(
+ conn.getHeaderField(HDR_LOCATION),
+ '/' + serviceName, redirects++);
+ try {
+ baseUrl = toURL(newUri);
+ } catch (MalformedURLException e) {
+ throw new TransportException(MessageFormat.format(
+ JGitText.get().invalidRedirectLocation,
+ uri, baseUrl, newUri), e);
+ }
+ continue;
+ default:
+ break;
+ }
+ }
+ break;
}
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
index 58081c1c9..35a1ee15e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/http/HttpConnection.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 Christian Halstrick
+ * Copyright (C) 2013, 2017 Christian Halstrick
* and other copyright owners as documented in the project's IP log.
*
* 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;
+ /**
+ * @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
*/
@@ -253,7 +273,7 @@ public void setRequestMethod(String method)
/**
* Configure the connection so that it can be used for https communication.
- *
+ *
* @param km
* the keymanager managing the key material used to authenticate
* the local SSLSocket to its peer